編輯:關於Android編程
一、開發背景:
我目前在做的是一個3年左右的老項目,項目開始的時候okhttp還不像現在這麼火,基本上使用HttpURLConnection類來實現所有的HTTP請求,當時采用的是xUtils框架來實現異步的,回調式的接口請求。現在發現xUtils這套框架存在幾個很大的問題。
老框架的性能問題:
1、xUtils的圖片加載任務會阻塞Http請求,因為xUtils中的圖片加載框架BitmapUtils和網絡請求框架HttpUtils的線程池是共用的,這個線程池的大小默認為3,也就是說當我在下載圖片的時候會阻塞Http請求數據接口的任務。這樣會帶來一個嚴重後果,當一個頁面圖片很多的時候,我打開一個新的頁面,新的頁面需要下載相應的json字符串來顯示,但是由於線程池裡滿滿的都是圖片下載任務,所以用戶必須等所有圖片都下載完畢之後才能調json的接口,本來很快就可以顯示的頁面現在卻要等無意義的圖片的下載,大大降低了用戶體驗。
2、xUtils框架連接握手太頻繁,根據抓包結果來看,xUtils在完成一次Http請求之後,會主動發送揮手的FIN報文,將TCP連接關閉。這樣的話如果短期內頻繁請求同一個服務器多次,那麼每次都要重新進行三次握手的步驟,浪費了許多時間,根據抓包結果來看,大約每次連接會浪費300ms左右的時間。抓包截圖如下
可以看出倒數第3行是由Android客戶端主動向服務器發送FIN報文,而且發送的時間是緊接著接口數據傳輸完畢後的。也就是說幾乎沒有進行連接保活,這樣如果短時間請求同一個接口多次的話,每次調用都會執行一次握手,大量的握手會消耗大量的時間,不適合目前APP會大量調用接口的情況。
3、Android 6.0發布之後,谷歌已經將所有舊版的HttpURLConnection,HttpClient,和一些和apache有關包的類和方法定義為過時方法,並且Android SDK 23之後不再內置舊版的類和接口,需要額外引用jar包,為了代碼的健壯性也需要拋棄舊版Android的HTTP框架。
OkHttp3.0的引入和配置:
在我的編程經驗裡來看,優秀的開源框架引入起來總不會是一帆風順的,OkHttp也是如此,這裡講講引入過程中的幾個大坑
首先在gradle裡添加引用(Eclipse可以下載連個jar包直接導入項目okhttp-3.3.1.jar,okio-1.8.0.jar):
compile 'com.squareup.okhttp3:okhttp:3.2.0'導入完打一個帶簽名的包馬上就會出問題
注意上面這些gradle編譯的報錯指示note,並不影響編譯進程,也不會影響打出來的apk包。這些note是progard在混淆的時候發現有重復的類報出來的,雖然不影響使用但是這裡還是要分析一下,去掉重復的類。報錯的矛頭指向舊版的舊版的HttpURLConnection和HttpClient的類,說它們重復了,重復的包主要是org.apache.commons.codec包。
於是我在項目裡搜索該包的引用,首先發現,我現在的編譯版本是23,但是23已經沒有這些類了,於是我添加了apache舊框架的支持jar包,在build.gradle裡是這樣配置的:
android { compileSdkVersion 23 buildToolsVersion "23.0.2" defaultConfig { applicationId "com.xxx.app" minSdkVersion 15 targetSdkVersion 17 versionCode 1 versionName '1.0.0.0' multiDexEnabled true } buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } productFlavors { } sourceSets { main { jniLibs.srcDirs = ['libs'] } } packagingOptions { exclude 'META-INF/LICENSE.txt' } useLibrary 'org.apache.http.legacy' ... }重點是這一句useLibrary 'org.apache.http.legacy'
他會將SDK目錄中\platforms\android-23\optional\org.apache.http.legacy.jar這個jar包自動添加到項目中來,而這個jar包裡面就有apache的一些類。
但它又是怎樣重復的呢?經過一番搜尋我又發現在xUtils這個第三方庫中有一個commons-codec.jar
正是這個jar包和org.apache.http.legacy.jar中的類出現了重復,於是把xUtils第三方庫中的commons-codec.jar刪掉,該note提示就會消除。
但是這樣仍然無法編譯,因為真正的問題是這個:
這個warning同樣是progard報出來的,而且不解決的話是無法打板的。網上說這個和nio有關的warning是okhttp在兼容Android SDK 24和Android M的過程中出現的,解決方法十分粗暴和野蠻,找到項目的progard-rules.pro 文件,添加這樣一行
-dontwarn okio.**也就是不要報任何和okio有關的異常,就可以繼續打包了,是不是十分粗暴。
二、OkHttp3的基本使用:
既然好不容易導入進來了,那麼就用一個簡單的GET和POST請求測試一下吧
加入我們要訪問這樣一個GET接口:http://staging.qraved.com:8033/app/home/timeline?cityId=1&max=10
那麼在使用okHttp的原生方法如下:
1、GET請求URL的拼裝:
首先要了解一下okHttp大體框架:一個HttpUrl封裝了一個請求的目標地址和相關參數,一個Request封裝了一次請求的所有相關信息,最後將Request對象交給OkHttpClient對象就可以執行連接服務器獲得數據流的過程。
方式1:我們可以向retrofit一樣,將協議,主機地址,接口地址,參數分開來寫,如下
HttpUrl httpUrl = new HttpUrl.Builder().scheme("http").host("staging.qraved.com").port(8033).addPathSegments("app/home/timeline").addQueryParameter("cityId","1").addQueryParameter("max","2").build(); Request request = new Request.Builder().url(httpUrl).get().build();
方式2:直接使用字符串作為GET的訪問地址
Request request = new Request.Builder().url("http://staging.qraved.com:8033/app/home/timeline?cityId=1&max=10").get().build()
下面我們用POST方法請求剛才的接口
這裡要注意一下,POST請求參數采用FormBody.Builder()進行封裝,這個和OkHttp2的類不同,一次POST請求的封裝如下
Request request = new Request.Builder().url("http://staging.qraved.com:8033/app/home/timeline").method("POST",new FormBody.Builder().add("cityId","1").add("max","2").build()).build();注意這裡的.method()方法,get請求是不需要寫這個函數的,如果該方法第一個參數是“GET”的話,那麼後面不要再跟參數,否則會報異常,因為GET請求不應該有body
3、異步執行網絡請求:
okHttp原生給出了同步請求和異步請求的方法,所謂同步就是會阻塞當前線程的任務,一般需要放在子線程中進行,異步調用使用的是接口進行回調,可以放在主線程裡進行,下面先介紹異步請求的方法(GET):
OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(httpUrl).get().build(); client.newCall(request).enqueue(new okhttp3.Callback() { @Override public void onFailure(okhttp3.Call call, IOException e) { Log.i("Alex","okhttp失敗",e); } @Override public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException { Log.i("Alex","okhttp成功"+response.body().string()); } });一般情況下,上面的respnse.body().string()就是請求一個json接口返回的json字符串。
4、同步執行網絡請求
如果想要更好的控制下載進度,推薦選用同步方法,不過要注意要放在子線程中執行:
new Thread() { @Override public void run() { super.run(); HttpUrl httpUrl = new HttpUrl.Builder().scheme("http").host("staging.qraved.com").port(8033).addPathSegments("app/home/timeline").addQueryParameter("cityId","1").addQueryParameter("max","2").build(); OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(httpUrl).get().build(); okhttp3.Response response = null; try { response = client.newCall(request).execute();//此時會阻塞線程 } catch (IOException e) { e.printStackTrace(); } if(response!=null){ String respBody = ""; try { respBody = response.body().string(); Log.i("Alex","請求結果是"+respBody); } catch (IOException e) { e.printStackTrace(); } } } }.start();我喜歡在線程池裡進行同步方法,對於線程池的調度後面再講。
好了okHttp的基礎就先到這,下面說一下Retrofit2如何配合okHttp3使用
三、Retrofit2的引入:
由於Retrofit2裡已經添加了對okHTTp的引用,所以我們不需要再在build.gradle中添加okhttp的引用了,只需要一句
compile 'com.squareup.retrofit2:retrofit:2.1.0'即可。
Retrofit2裡有很多的類名字與okHttp3一模一樣,他們成員方法也幾乎一模一樣,這是Retrofit的故意設計,值得一提的是,Retrofit2也由Call類,而且與okhttp的Call用法幾乎一樣,都支持.cancel()方法,可以隨意停止正在進行的下載任務,十分好用。
使用Retrofit發送GET和POST請求
Retrofit2更像是一個注解框架,他對網絡訪問的常用操作進行了封裝,會使代碼看起來格外簡潔易懂,一開始上手會有點不適應(起始時間長了也會感覺怪怪的,可能我個人不太喜歡注解框架的原因吧)
首先需要對URL進行封裝,這裡需要我們新建一個接口,還是以上面的url為例,這個接口規定了請求方式是GET還是POST,規定了請求參數的key和數量,規定了接口的地址,如下
import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; /** * Created by Administrator on 2016/6/27. */ public interface TestInterface { //接口示例 http://staging.qraved.com:8033/app/home/timeline?v=2.5.7&client=1&cityId=2&userId=6092&max=10&minId=0 @GET("/app/home/timeline?")//設置是get請求還是post請求 Call如果只是想從接口獲得一個字符串,那麼Call的泛型就可以填ResponseBody,如果想json解析成一個對象,那麼這裡就填該類的泛型listRepos(@Query("v") String v, @Query("client") String client, @Query("userId") String userId, @Query("minId") String minId, @Query("max") String max); }
然後就應該填入相關參數,請求網絡了,注意Retrofit將主機地址和接口的地址分開了,方便我們靈活的切換服務器,如下:
Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://staging.qraved.com:8033")//這裡填入主機的地址 .build(); TestInterface service = retrofit.create(TestInterface.class); Callcall = service.listRepos("2.5.7", "1", "6092", "0", "3"); Log.i("Alex", "body是" + call.request().body() + " url是" + call.request().url() + " method是" + call.request().method()); call.enqueue(new Callback () { @Override public void onResponse(Call call, Response response) { try { Log.i("Alex", "成功" + response.body().string()); } catch (IOException e) { e.printStackTrace(); } } @Override public void onFailure(Call call, Throwable t) { Log.i("Alex", "失敗", t); } });
為了兼容一些老型號的舊設備,尤其是一些內存小的安卓手機,我們不能無限制的新增線程,根據上面說的okhttp的同步方法,每次請求都應該放在一個子線程裡進行操作,所以現在就需要一個線程池,然後讓所有的網絡請求組成一個隊列,然後按順序一組一組的執行,這樣可以減少線程頻繁創建和銷毀的開銷,減少CPU和內存的壓力。
但是一個線程池是不夠的,假設我們要頻繁的請求很多接口,而中間有一個接口耗時特別的長,接近60s,那麼這個非常耗時的請求就會阻塞線程池中的某個線程很長一段時間,導致其他請求不能按時得到結果,如果這種耗時請求再多有幾個的話,用戶就會覺得做什麼操作都很卡,即使一些很小很快的接口也會被大接口擋住。
我在項目中的解決方法是,將和UI有關的,需要即時反應的接口放在線程池裡進行,將後台操作的網絡請求,和用戶關系不大的網絡請求比如追蹤事件,獲取更新什麼的還有一些特別耗時的接口放在一個單線程中執行,首先保證UI界面的快速顯示。有這樣一快一慢兩個線程池,帶給用戶的使用體驗會大幅提升。
五、關於連接保活的優化:
上面提到xUtils會在數據請求完畢之後馬上主動發送一個FIN報文將當前的tcp連接關掉,但是okHttp號稱可以根據服務器的壓力自動進行連接保活,通過我的抓包結果來看,okhttp實際上是沒有主動發送FIN報文,等待服務器來主動要求關閉連接,okHttp也不會發送keep-Alive報文,在服務器的FIN報文到達後,okHttp會關閉當前的連接。如果在連接超時之前客戶端發送新的請求,那麼連接會保持,這樣通過一個tcp端口,只進行一次三次握手的情況下連續請求多個接口或者一個接口請求多次,節省了三次握手的時間,進而減少了用戶等待的時間,下面的抓包截圖展示了只進行一次三次握手,然後連續三次調用同一個接口,並在最後一次調用結束後保活10s的情況
六、關於及時停止沒有意義的網絡請求:
有的時候一些網絡請求可能會成為廢請求,它們不僅擠占了線程池資源,占了內存,多跑了流量,多費了電,甚至會造成內存洩漏。比如用戶打開了一個Activity,這個Activity開啟了許多個網絡請求,有些請求耗時還特別多,可是用戶看了沒兩眼就把這個Activity關掉去看別的了,但是這些網絡請求還在一個個按部就班的執行,這是毫無意義的。由於okHttp我最喜歡的cancel功能,這些問題可以迎刃而解了。
這裡講一下github上一個叫okhttpfinal的框架的處理邏輯:
它有一個HashMap
順便提一下這個okhttpfinal的一個很不好的地方,它使用一個線程(就是系統AsynTask)執行所有的網絡請求,如果有一個請求特別耗時,就會堵塞其他的請求,我在使用這個框架的時候修改其源碼換成兩個線程池來解決這個問題。
最近的一些學習心得:功能實現:點擊圓形頭像之後可以實現相冊上傳或者開啟相機,然後把得到的圖片經過剪裁,把剪裁過的圖片設置為頭像的背景圖步驟:第一步:自定義一個類,繼承Im
本文由阿裡巴巴移動安全客戶端、YunOS資深工程師Hao(嵌入式企鵝圈原創團隊成員)撰寫,是Hao在嵌入式企鵝圈發表的第一篇原創文章,對Android無線開發
本文實例介紹了Android實現粒子雨效果的實現過程,分享給大家供大家參考,具體內容如下先看看效果圖:具體實現方法:1.baseview主要是設定雨滴要實現的動作,只是先
1、checkbox(復選框)checkbox常用於多選的情況,比如批量刪除、添加群聊等;DOM結構 checkbox示例 默認checkbox在右側顯示