面对一个项目,对于android应用开发框架的选择,我想过三种方案:
1.使用loader + httpclient + greendao + gson + fragment,优点是可定制性强,由于使用google家自己的loader和loadermanager,代码健壮性强。
缺点是整套代码学习成本较高,使用过程中样板代码较多,(比如每一个request都需要产生一个新类)
2.volley,作为google在io大会上得瑟过的一个网络库,其实不算什么新东西(2013 io发布),使用较为简单,请求可以取消,可以提供优先级请求,看起来还是不错的。
3.retrofit,一款为了使请求极度简单化的rest api client,呼声也很高,使用门槛几乎是小白型。
如何选择呢?首先干掉1,因为对新人的学习成本确实太高,如果要快速开发一个项目,高学习成本是致命的,同时使用起来样板代码很多。
那么如何在volley和retrofit中选择呢?尽管网上有很多文章在介绍两个框架的使用方法,而对于其原理,特别是对比分析较少,如果你手里有一个项目,如何选择网络模块呢?
首先说明一下这两个网络框架在项目中的层次:
从上图可知,不管volley还是retrofit,它们都是对现有各种方案进行整合,并提供一个友好,快速开发的方案,在整合过程中,各个模块都可以自行定制 或者替换。比如反序列化的工作,再比如httpclient。
而在本文我们将简略地来看一下retrofit的源码部分。
注意,本文并不是使用retrofit的帮助文档,建议先看retrofit的文档和okhttp的文档,这些对于理解余下部分很重要。
使用retrofit发送一个请求
假设我们要从这个地址 http://www.exm.com/search.json?key=retrofit中获取如下json返回:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
{ "data" : [ { "title" : "retrofit使用简介" , "desc" : "retrofit是一款面向android和java的httpclient" , "link" : "http://www.exm.com/retrofit" }, { "title" : "retrofit使用简介" , "desc" : "retrofit是一款面向android和java的httpclient" , "link" : "http://www.exm.com/retrofit" }, { "title" : "retrofit使用简介" , "desc" : "retrofit是一款面向android和java的httpclient" , "link" : "http://www.exm.com/retrofit" } ] } |
1.引入依赖
1
2
3
|
compile 'com.squareup.retrofit:retrofit:2.0.0-beta2' //gson解析 compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2' |
2.配置retrofit
1
2
3
4
5
|
retrofit retrofit = new retrofit.builder() .baseurl( "http://www.exm.com" ) .addconverterfactory(gsonconverterfactory.create()) .client( new okhttpclient()) .build(); |
3.新建model类 searchresult来解析结果
4.新建请求接口
retrofit使用注解来定义一个请求,在方法上面指定请求的方法等信息,在参数中指定参数等信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public interface restapi { @get ( "/search.json" ) call<list<searchresult>> search( @query ( "key" ) string key ); //可以定义其它请求 @get ( "/something.json" ) call<something> dosomething( @query ( "params" ) long params ....... ....... ); } |
5.发送请求,我们可以发送同步请求(阻塞当前线程)和异步请求,并在回调中处理请求结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
restapi restapi = retrofit.create(restapi. class ); call<list<searchresult>> searchresultscall = resetapi.search( "retrofit" ); //response<list<searchresult> searchresults = searchresultscall.execute(); 同步方法 searchresultscall.enqueue( new callback<list<searchresult>>() { @override public void onresponse(response<list<searchresult>> response, retrofit retrofit) { content.settext(response.body().tostring()); } @override public void onfailure(throwable t) { content.settext( "error" ); } }); |
retrofit源码分析
retrofit整个项目中使用了动态代理和静态代理,如果你不太清楚代理模式,建议先google一下,如果对于java的动态代理原理不是太熟悉,强烈建议先看:这篇文章-戏说代理和java动态代理
ok,下面按照我们使用retrofit发送请求的步骤来:
1
|
restapi restapi = retrofit.create(restapi. class ); //产生一个restapi的实例 |
输入一个接口,直接输出一个实例。
这里岔开说一句,现在随便在百度上搜一下java动态代理,出来一堆介绍aop(面向切面编程)和spring,导致一部分人本末倒置,认为动态代理几乎等于aop,甚至有些人认为动态代理是专门在一个函数执行前和执行后添加一个操作,比如统计时间(因为现在几乎所有介绍动态代理的地方都有这个例子),害人不浅。实际上动态代理是jdk提供的api,并不是由这些上层建筑决定的,它还可以做很多别的事情,retrofit中对动态代理的使用就是佐证。
搂一眼这里的源码,再次建议,如果这里代码看不明白,先看看上面提到的那篇文章:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public <t> t create( final class <t> service) { //返回一个动态代理类的实例 return (t) proxy.newproxyinstance(service.getclassloader(), new class <?>[] { service }, //这个invocationhandler是关键所在,以后调用restapi接口中的方法都会被发送到这里 new invocationhandler() { private final platform platform = platform.get(); @override public object invoke(object proxy, method method, object... args) throws throwable { /* 如果是object的方法,如tostring()等,直接调用invocationhandler的方法, *注意,这里其实是没有任何意义的,因为invocationhandler其实是一个命令传送者 *在动态代理中,这些方法是没有任何语义的,所以不需要在意 */ if (method.getdeclaringclass() == object. class ) { return method.invoke( this , args); } //对于java8的兼容,在android中不需要考虑 if (platform.isdefaultmethod(method)) { return platform.invokedefaultmethod(method, service, proxy, args); } //返回一个call对象 return loadmethodhandler(method).invoke(args); } }); } |
我们可以看到retrofit.create()之后,返回了一个接口的动态代理类的实例,那么我们调用这个代理类的方法时,调用自然就被发送到我们定义的invocationhandler中,所以调用
1
|
call<list<searchresult>> searchresultscall = resetapi.search( "retrofit" ); |
时,直接调用到invocationhandler的invoke方法,下面是invoke此时的上下文:
1
2
3
4
5
6
7
8
9
10
11
|
@override public object invoke(object proxy, method method, object... args) throws throwable { //proxy对象就是你在外面调用方法的resetapi对象 //method是restapi中的函数定义, //据此,我们可以获取定义在函数和参数上的注解,比如@get和注解中的参数 //args,实际参数,这里传送的就是字符串"retrofit" //这里必然是return 一个call对象 return loadmethodhandler(method).invoke(args); } |
此时,invoke的返回必然是一个call,call是retrofit中对一个request的抽象,由此,大家应该不难想象到loadmethodhandler(method).invoke(args); 这句代码应该就是去解析接口中传进来的注解,并生成一个okhttpclient中对应的请求,这样我们调用searchresultscall时,调用okhttpclient走网络即可。确实,retrofit的主旋律的确就是这样滴。
注意,实际上call,callback这种描述方式是在okhttp中引入的,retrofit底层使用okhttp所以也是使用这两个类名来抽象一个网络请求和一个请求回来之后的回调。总体来看,retrofit中的call callback持有一个okhttp的call callback,将对retrofit中的各种调用转发到okhttp的类库中,实际上这里就是静态代理啦,因为我们会定义各种代理类,比如okhttpcall
注 下文中如不无明确支出,则所有的call,callback都是retrofit中的类
methodhandler类
methodhandler类,它是retrofit中最重要的抽象了,每一个methodhandler对应于本例的restapi中的一个每个方法代表的请求以及和这个请求相关其它配置,我们来看看吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//这个okhttp的工厂,用于产生一个okhttp类库中的call,实际上就是传入配置的builder的okhttpclient private final okhttp3.call.factory callfactory; //通过method中的注解和传入的参数,组建一个okhttp的request private final requestfactory requestfactory; //用于对retrofit中的call进行代理 private final calladapter<?> calladapter; //用于反序列化返回结果 private final converter<responsebody, ?> responseconverter; // 返回一个call对象 object invoke(object... args) { return calladapter.adapt( new okhttpcall<>(callfactory, requestfactory, args, responseconverter)); } |
在retrofit中通过添加converter.factory来为retrofit添加请求和响应的数据编码和解析。所以我们可以添加多个converter.factory为retrofit提供处理不同数据的功能。
calladapter 可以对执行的call进行代理,这里是静态代理。我们也可以通过添加自己的calladapter来作一些操作,比如为请求加上缓存:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
new calladapter.factory { @override public <r> call<r> adapt(call<r> call) { return call;} } class cachecall implements call { call delegate; cachecall(call call) { this .delegate = call; } @override public void enqueue(callback<t> callback) { //查看是否有缓存,否则直接走网络 if (cached) { return cache; } this .delegate.enqueue(callback); } } |
至此,我们在调用resetapi.search("retrofit");时,实际上调用的层层代理之后的okhttpcall,它是methodhandler中invoke的时候塞入的。看看okhttpcall中的代码吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
@override public void enqueue( final callback<t> callback) { okhttp3.call rawcall; try { //创建一个okhttp的call rawcall = createrawcall(); } catch (throwable t) { callback.onfailure(t); return ; } //直接调用okhttp的入队操作 rawcall.enqueue( new okhttp3.callback() { private void callfailure(throwable e) { try { callback.onfailure(e); } catch (throwable t) { t.printstacktrace(); } } private void callsuccess(response<t> response) { try { callback.onresponse(response); } catch (throwable t) { t.printstacktrace(); } } @override public void onfailure(request request, ioexception e) { callfailure(e); } @override public void onresponse(okhttp3.response rawresponse) { response<t> response; try { //解析结果 response = parseresponse(rawresponse); } catch (throwable e) { callfailure(e); return ; } callsuccess(response); } }); } |
如此一来,okhttpclient的回调也被引导到我们的callback上来,整个流程就已经走通了。
总结
终于到了总结的时候了,一般来说,这都是干货的时候,哈哈~
volley对比优势
1.缓存处理;volley自己就提供了一套完整的缓存处理方案,默认使用文件存储到磁盘中,并且提供了ttl softttl这么体贴入微的机制;一个request可能存在两个response,对于需要显示缓存,再显示网络数据的场景真是爽的不要不要的。而retrofit中并没有提供任何和缓存相关的方案。
2.代码简单,可读性高。volley的代码是写的如此的直接了当,让你看起代码来都不需要喝口茶,这样的好处是我们我们需要修改代码时不太容易引入bug....囧
同一个请求如果同时都在发送,那么实际上只会有一个请求真正发出去, 其它的请求会等待这个结果回来,算小小优化吧。实际上这种场景不是很多哈,如果有,可能是你代码有问题...
请求发送的时候提供了优先级的概念,但是是只保证顺序出去,不保证顺序回来,然并卵。
支持不同的http客户端实现,默认提供了httpclient和httpurlconnection的实现,而retrofit在2.0版本之后,锁死在okhttp上了。
3.支持请求取消
retrofit
1.发送请求真简单,定义一个方法就可以了,这么简单的请求框架还有谁?volley?
2.较好的可扩展性,volley中每一个新建一个request时都需要指定一个父类,告知序列化数据的方式,而retrofit中只需要在配置时,指定各种转换器即可。calladapter的存在,可以使你随意代理调用的call,不错不错。。。
3.okhttpclient自带并发光环,而volley中的工作线程是自己维护的,那么就有可能存在线程由于异常退出之后,没有下一个工作线程补充的风险(线程池可以弥补这个缺陷),这在retrofit中不存在,因为即使有问题,这个锅也会甩给okhttp,嘿嘿
4.支持请求取消
再次说明的是,retrofit没有对缓存提供任何额外支持,也就是说它只能通过http的cache control做文件存储,这样就会有一些问题:
1.需要server通过cache control头部来控制缓存,需要修改后台代码
2.有些地方比较适合使用数据库来存储,比如关系存储,此时,retrofit就无能为力了
3.缓存不在我们的控制范围之内,而是完全通过okhttp来管理,多少有些不便,比如我们要删除某一个指定的缓存,或者更新某一个指定缓存,代码写起来很别扭(自己hack请求头里面的cache contrl)
而在我们项目的实际使用过程中,缓存是一个比较重要的角色,retrofit对缓存的支持度不是很好,真是让人伤心。。。
但是,我们还是觉得在使用中retrofit真心比较方便,容易上手,通过注解代码可读性和可维护性提升了n个档次,几乎没有样板代码(好吧,如果你觉得每个请求都需要定义一个方法,那这也算。。),所以最后的决定是选择retrofit。
有人说了,volley中的两次响应和缓存用起来很happy怎么办?嗯,我们会修改retrofit,使它支持文件存储和orm存储,并将volley的缓存 网络两次响应回调移接过来,这个项目正在测试阶段,待我们项目做完小白鼠,上线稳定之后,我会把代码开源,大家敬请关注。