Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android Okhttp3+Retrofit2網絡加載效率優化

Android Okhttp3+Retrofit2網絡加載效率優化

編輯:關於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();

這樣做首先通過HttpUrl.Builder()采用工廠模式生產出一個HttpUrl對象,在將這個對象放到一個Request對象中。

 

方式2:直接使用字符串作為GET的訪問地址

Request request = new Request.Builder().url("http://staging.qraved.com:8033/app/home/timeline?cityId=1&max=10").get().build()

2、POST請求URL和參數的拼裝:

 

下面我們用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字符串。
注意client.newCall(request).enqueue()方法是一個異步方法,返回值是一個Call對象,相當於一個請求過程,這個Call對象有一個cancel()方法,可以取消當前進度,讓出相應的系統資源,釋放內存,降低CPU開銷,當我們需要停止某次HTTP請求的時候是一個非常方便的方法。

 

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 listRepos(@Query("v") String v, @Query("client") String client, @Query("userId") String userId, @Query("minId") String minId, @Query("max") String max);
}
如果只是想從接口獲得一個字符串,那麼Call的泛型就可以填ResponseBody,如果想json解析成一個對象,那麼這裡就填該類的泛型

 

然後就應該填入相關參數,請求網絡了,注意Retrofit將主機地址和接口的地址分開了,方便我們靈活的切換服務器,如下:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://staging.qraved.com:8033")//這裡填入主機的地址
                .build();
        TestInterface service = retrofit.create(TestInterface.class);
        Call call = 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>,它的key是一個用於識別Activity和Fragment的字符串,這個是自己定的,一般可以使用activity.getClass().getName()作為key,value是一個數組,這個數組是所有該Activity或Fragment建立網絡請求。在我要finish一個Activity或fragment的時候,我就將key傳入HashMap拿到這個Activity的所有請求,然後一個一個的全部取消,這樣就解決了無用請求的問題。

順便提一下這個okhttpfinal的一個很不好的地方,它使用一個線程(就是系統AsynTask)執行所有的網絡請求,如果有一個請求特別耗時,就會堵塞其他的請求,我在使用這個框架的時候修改其源碼換成兩個線程池來解決這個問題。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved