Retrofit 的神秘面纱

初识Retrofit时,觉得很神奇,体现在以下两点:

神奇之一:仅凭注解和接口便可执行网络请求。

public interface GithubService {

    @GET("users/{username}")
    Call<User> getUser(@Path("username") String username);

}

神奇之二:接口方法的返回值变幻无穷。

无论你是用默认的方式,或者是RxJava,无论是在Java或是Android中调用,Retrofit均可满足,体现在接口的返回值,如下代码:

public interface GithubService {

    //默认方式
    @GET("users/{username}")
    Call<User> getUserByDefault(@Path("username") String username);

    //RxJava方式
    @GET("users/{username}")
    Observable<User> getUserByRxJava(@Path("username") String username);

}

这两个神奇之处,犹如两层神秘面纱,将Retrofit原本俊俏娇羞的脸庞,遮掩得若隐若现。若不揭开面纱,一睹芳容,便无处释放程序员们最原始的冲动,于是一起来阅读源码吧。

寻找接口的实现类

对于第一个神奇之处,首先我们来看看GithubService接口是如何调用的:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);
Call<User> call = githubService.getUser("geniusmart");

仔细斟酌这段代码,GitHubService本身是个接口,并调用了getUser()方法获得返回值,那么问题来了:

  1. GitHubService是由我们定义的接口,但是我们并没有定义实现类,实现类到底在哪里?
  2. 既然我们没有定义实现类,那么大胆假设该实现类是由Retrofit框架提供的。对于框架而言,该接口是由开发者自定义的,框架无法做到未卜先知,那么他是如何做到的?

以上两个问题可以总结为:框架如何动态生成GitHubService的实现类?答案也不难猜测——动态代理模式。(对动态代理有疑问的同学,可以先看下《公共技术点之 Java 动态代理》

接下来验证我们的猜测,GitHubService的来源在此行代码中:

GitHubService service = retrofit.create(GitHubService.class);

查看create()的源码,所有谜底都将揭开:

通过JDK实现动态代理

这个方法生成并返回了GitHubService的动态代理对象,在执行接口的每个方法时,实际执行的是invoke(),而该方法里的逻辑便是此框架的主体流程,如下代码:

public Object invoke(Object proxy, Method method, Object... args){
    //1.负责注解的解析
    ServiceMethod serviceMethod = loadServiceMethod(method);
    //2.负责与OKHttp3的对接,处理同步和异步发送网络请求
    OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
    //3.第二层面纱,下文再做解释
    return serviceMethod.callAdapter.adapt(okHttpCall);
}

这个方法里,做了三件事情:

  1. 解析方法的注解,比如get/post、url、Headers等,解析的结果存放于ServiceMethod对象中。
  2. 创建OkHttpCall,该对象负责与OKHttp3对接,处理同步或异步的网络请求。
  3. 这是第二层神秘面纱的谜底,下文再做解释。

至此,我们揭开了Retrofit的第一层神秘面纱,这是一位简单而优雅的小女子,表面朴实无华,恬静清新,令人迫不及待想要揭开她的第二层面纱,知晓她的内心世界。

变幻无穷的适配

首先来回顾下上文提到的第二个神秘之处,同样是获取用户信息的网络请求,可以返回Call<User>Observable<User>,这是如何做到的?

上文中,invoke()做的第三件事情便是获取返回值,如下:

return serviceMethod.callAdapter.adapt(okHttpCall);

其中,adapt是适配的意思,其目的是变废为宝,将指定的输入类型适配成实际需要的输出类型,来查看下它的源码:

public interface CallAdapter<T> {
  <R> T adapt(Call<R> call);
}

我们重点关注下adapt的输入输出。这里的设计非常大胆,输入是有约束的,即 Call类型,而输出为泛型T,也就是说输出是没有任何约束的,如此一来,扩展性极强,可以指定任何的类型。

剩下来的问题便是:

  1. adapt由哪个对象触发,即CallAdapter的具体实现是什么?
  2. 输入类型Call的具体实现是什么?
  3. 输出类型T的具体实现是什么?

这里直接给出答案,如下两图:

默认方式的适配
RxJava方式的适配
  • 通过上面两图,可以看到,输入对象是固定的,即OkHttpCall对象,网络请求实际上便是由该对象发起的。
  • 默认方式的输出对象是ExecutorCallbackCall,它持有OkHttpCall对象,指定了网络请求执行结束后的回调函数在UI线程中执行。
  • RxJava方式的输出对象是Observable,持有该对象便具备函数式编程的能力。
  • adapt()的调用者——适配器CallAdapter是动态配置的,默认方式无需配置,如果使用RxJava,则需要配置适配器的工厂对象,如下代码:
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();

这样设计简直是解耦得一塌糊涂,我们可以任意变换调用方式,也可以任意扩展CallAdapter来定义新的调用方式,而框架无需做任何调整。比如JakeWharton前几天开源的RxJava2的适配——retrofit2-rxjava2-adapter,比如针对Google函数式编程库 Agera的适配——retrofit-agera-call-adapter,简直OCP得一塌糊涂。

至此,第二层神秘面纱缓缓而落,这是一位心灵手巧的奇女子,你给她一针一线,她还你一幅刺绣,刺绣上可以是锦绣山河,也可以是紫气东来;你给她新鲜的食材,她变幻出色香味俱全的美味佳肴。

总结

动态代理、适配是Retrofit的核心,理解清楚这两点,再去梳理框架的其他细节,方可事半功倍。除此之外,框架内部使用了大量的Builder模式和Factory模式,这两个模式使得创建对象和获取对象更有条理,更清晰易懂。值得一提的是,这个框架配备了完整的单元测试用例,非常值得我们学习。

参考文章

http://square.github.io/retrofit/
https://github.com/square/retrofit

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容