您的位置:首页 > 理论基础 > 计算机网络

Android Okhttp3+Retrofit2网络加载效率优化

2016-06-30 17:00 453 查看
一、开发背景:

我目前在做的是一个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://www.mydomin.com:8088/android/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("www.mydomin.com").port(8088).addPathSegments("android/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://www.mydomin.com:8088/android/home/timeline?cityId=1&max=10").get().build()

2、POST请求URL和参数的拼装:

下面我们用POST方法请求刚才的接口

这里要注意一下,POST请求参数采用FormBody.Builder()进行封装,这个和OkHttp2的类不同,一次POST请求的封装如下

Request request = new Request.Builder().url("http://www.mydomin.com:8088/android/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("www.mydomin.com").port(8088).addPathSegments("android/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://www.mydomin.com:8088/android/home/timeline?v=2.5.7&client=1&cityId=2&userId=6092&max=10&minId=0 @GET("/android/home/timeline?")//设置是get请求还是post请求
Call<ResponseBody> 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://www.mydomin.com:8088")//这里填入主机的地址
.build();
TestInterface service = retrofit.create(TestInterface.class);
Call<ResponseBody> 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<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
try {
Log.i("Alex", "成功" + response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.i("Alex", "失败", t);
}
});

四、Retrofit2配合RxJava使用:

首先要在gradle中导入RxJava相关的类库

compile 'io.reactivex:rxjava:1.0.14'
compile 'io.reactivex:rxandroid:1.0.1'
compile 'com.squareup.okhttp3:okhttp:3.2.0'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'然后网络请求接口的写法也与上面的不一样,返回值已经不再是Call<>,而是RxJava中的一个Observerble对象,如下
import okhttp3.ResponseBody;
import retrofit2.http.GET;
import retrofit2.http.Query;
import rx.Observable;

public interface TestInterface {
@GET("/android/home/timeline?")
Observable<ResponseBody> getHomeTimeLine(@Query("minId") String minId, @Query("max") String max);
}接收回调的方式也与上面不一样
CallAdapter.Factory rxJavaCallAdapterFactory = RxJavaCallAdapterFactory.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.example.com:8033")//这里填入域名
.addCallAdapterFactory(rxJavaCallAdapterFactory)
.build();
TestInterface service = retrofit.create(TestInterface.class);
service.getHomeTimeLine("0","3")//填入相关的请求参数
.doOnNext(new Action1<ResponseBody>() {
@Override
public void call(ResponseBody responseBody) {//子线程执行
Log.i("Alex","doOnnext线程是"+Thread.currentThread().getName()+"线程优先级="+Thread.currentThread().getPriority()+" 线程id="+Thread.currentThread().getId());
//在这里可以做一些预处理的动作,但是不要调用responseBody的方法,因为多数方法都是一次性的
}
})
.subscribeOn(Schedulers.io())//这个必须写,否则会报network main thread异常
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<ResponseBody>() {
@Override
public void onNext(ResponseBody body) {//主线程执行
Log.i("Alex","subscribe线程是"+Thread.currentThread().getName()+"线程优先级="+Thread.currentThread().getPriority()+" 线程id="+Thread.currentThread().getId());
try {
Log.i("Alex","最终的结果是"+body.string());//.string()方法只能用一次,第二次用得到的结果就是空字符串
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void onCompleted() {
}

@Override
public void onError(Throwable error) {
// Error handling
}
});

这里要注意,ResponseBody.String()方法只有第一次调用有效,第二次调用时由于输入流已经被关闭,所以将会获得空字符串,使用RxJava的方式,可以轻松的处理多重回调,或者在子线程中执行一些网络请求结束后的耗时操作,然后回调主线程修改UI,比如我从服务器获得相应json字符串之后,进行一些数据库存取操作或者过滤操作,这些都可以很容易的放在子线程中进行。
如果相对获得的json字符串进行进一步的处理或者做一些数据库操作等耗时任务,推荐将Observerble<ResponseBody>转换为Observerble<String>,使用flatMap()函数就能完成这个转换,这样我们就可以突破Responsbody中.string()只能使用一次的限制了

service.getHomeTimeLine("0","3")//填入相关的请求参数
.flatMap(new Func1<ResponseBody, Observable<String>>() {
@Override
public Observable<String> call(ResponseBody responseBody) {//子线程中执行
String json = "";
try {
json = responseBody.string();
Log.i("Alex","json是"+json);
} catch (IOException e) {
e.printStackTrace();
}
Observable<String> o = Observable.just(json);
o.map(new Func1<String, String>() {
@Override
public String call(String s) {//子线程中执行
s = s+"hhh";//在子线程中做一些字符串的处理或者数据库耗时操作
return s;
}
})
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<String>() {
@Override
public void call(String s) {
//主线程中执行
TextView tv1 = (TextView) findViewById(R.id.tv1);
tv1.setText(s);
}
});
return o;
}
})
.subscribeOn(Schedulers.io())//这个必须写,否则会报network main thread异常
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
五、关于线程调度的优化:

为了兼容一些老型号的旧设备,尤其是一些内存小的安卓手机,我们不能无限制的新增线程,根据上面说的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<String,List<Call>>,它的key是一个用于识别Activity和Fragment的字符串,这个是自己定的,一般可以使用activity.getClass().getName()作为key,value是一个数组,这个数组是所有该Activity或Fragment建立网络请求。在我要finish一个Activity或fragment的时候,我就将key传入HashMap拿到这个Activity的所有请求,然后一个一个的全部取消,这样就解决了无用请求的问题。

顺便提一下这个okhttpfinal的一个很不好的地方,它使用一个线程(就是系统AsynTask)执行所有的网络请求,如果有一个请求特别耗时,就会堵塞其他的请求,我在使用这个框架的时候修改其源码换成两个线程池来解决这个问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: