一、浅谈Okhttp拦截器
添加一个自定义拦截器很简单,只需要实现 Okhttp 的 Interceptor 接口,重写其中的intercept 方法,最后在 OkHttpClient.Builder 链式代码中注册和添加这个拦截器,为什么要称作“自定义“拦截器呢?因为 Okhttp 核心实现就是基于其内部五大拦截器,我们下文再说。
需要注意的是,Retrofit 是基于 Okhttp 的封装,所以当我们谈论“Retrofit的拦截器”,其实就是OKhttp的拦截器。
先看一下上一节,我们用 Okhttp 实现网络请求的 demo
对照着看一下 Okhttp 发送请求的执行流程
我们分析大红框里的内容,这张图有些繁琐,也可以转化成下图。
还是直接上图,就像流水线工厂一样,一个一无所有的 Request,依照顺序依次经过五大拦截器的前置拦截,最后拿到response后,再倒序经过五大拦截器的后置拦截。
我们引用@yuashuai 所举的例子,他表述的既生动又贴切。
老张有很多干面条,但是他想吃汤面,可是自己又不会做,但是碰巧村里大郎会做,于是老张拿一包干面条让大郎做成了汤面。但是老张发现他做面不好吃,盐都没放,连个青菜叶子都没有。
这时候老张正好碰到隔壁老王,老王说了这东西我也会做,比他做的好吃多了。于是老张又拿着一包干面条给了老王,老王说老张你等着,我马上回家给你做,做好了就给你送过去。但是老王回家并没有做,而是去家里拿了一包盐,然后去找隔壁老李了,原来老王并不会做面,但是他知道隔壁老李会做,而且做得比较好吃。于是他把干面条和盐都交给了老李。老李对老王说你回去等着吧,做好了马上给你送过去。可是老李同样不会做,但是他知道村里的大郎会做,这时老李首先回厨房拿了两个生菜叶子,然后带着老王给的干面条和盐去找大郎了,对大郎说,生菜叶子,盐,面条都给你了,你快给我做一碗面。大郎对老李说好嘞,3分钟就好了,3分钟后,老李拿着做好了的放了盐和生菜叶子的一碗面回去了。本来打算直接给老王,但是一想,自己放了两个生菜叶子,不吃点这个面吃不是有点亏,于是老李偷偷了吃了几根面。然后老李去找老王说你的面做好了并把面交给了老王。老王一看这面只有两个青菜叶子,营养是不是不够呀!于是老王又买了半斤熟牛肉,切切放了进去。然后老王去找老张说你的面做好了,还说道这么大一碗你也吃不完吧,让小张也吃点。最后老张吃着老王送来的红烧牛肉面感动的肉牛满面。
这里的干面条就可以看做一个最原始的 request,到老王哪里被加了点盐,到老李哪里被加了生菜叶子,于是大郎才能把这个request做成放了盐和生菜叶子的response,这个response回到老李哪里又被啃了几口,到老王哪里又被放了点牛肉。于是最后回到老张那里收到的response就是被扣了几口并且加了牛肉的response。这样整个链条是不是就清楚了!
@Wanghao 画了一张非常全面的图。
在第三讲的末尾我们讨论过 BridgeInterceptor 拦截器,它会对我们我们配置的 request请求补充头信息,比如请求类型、UA,如果你自己不加ua,Bridge 拦截器就会添加“okhttp/版本号”这样的ua。
这是 BridgeIntercetpor 的源码
我们也可以自定义拦截器,分为应用级别拦截器和网络级别拦截器,在 @Wanghao 的图上我们可以看到它们分别作用的时机。
1.应用级别拦截器:只会调用一次,获取到最终的 response 结果
2.网络级别拦截器:可以感知到网络的请求的重定向,以及重试会被执行多次
这两种拦截器在注册方式各不同,分别调用 addInterceptor()以及 addNetworkInterceptor 方法进行注册,但实现拦截器的步骤没有不同,所以我们不做过多笔墨。
二、分析一个简单的拦截器
我们随便找一个拦截器 demo 分析一下,我们需要熟悉拦截器的具体操作,找到它代码流程上的一些特征,然后根据这些特征在Jadx中搜索反编译后的 Java 代码,去寻找某书实现添加sign等等11个参数的拦截器。
除此之外还有个思路,上文说到,自定义的拦截器分为 Application 拦截器和 Network 拦截器,但不管哪一种,都需要在 OkHttpClient.Builder 链式代码中通过 addInterceptor/addNetworkInterceptor 方法添加和注册后才能生效,那我们可以全局搜索 addInterceptor 以及 addNetworkInterceptor,然后得到拦截器的线索,这样应该也是可以的。我们先看看当前的这个思路。
我们可以看到三个明显的特征:
一: 是实现Interceptor接口 “public class xxx(自定义的拦截器名) implements Interceptor {”
二: 是重写intercept方法“public (可能有修饰符) Response intercept(Chain Chain)……”
三: 是添加字段时调用的 “.addQueryParameter” 方法(这儿我们加了个“.”,可以缩小检索范围),如果是添加Header 头信息,则会调用 ”.header“ 这个方法
如果 App 没有对 Okhttp 类进行混淆,那我们就可以根据这三个特征找到 Retrofit 的拦截器实现。
我们这儿再多说两句,看图中第九行,Request originalRequest = chain.request(); 这个demo作者为什么要将这个Request命名为 originalRequest(original:原先的,原始的)呢?
再回想一下拦截器的工作原理。想象一下一个流水线,request 是材料,response 是产品,从头到尾经历了许多次加工(拦截器)。每个拦截器先通过 chain.request() 得到这个 reqeust,经过一系列操作后的 request,再放回 chain.proceed 方法,最后return 回去,request 经过了一次洗礼,等待它的是不断的新的洗礼。
chain.proceed 这个方法名非常精准和有神,Okhttp采用了设计模式中的责任链模式,感兴趣的可以看一下这篇关于责任链的文章https://www.cnblogs.com/aeolian/p/8888958.html。
因此,对于每一个拦截器来说,取到的request就是原始和过去的,命名就可能会用”old“、”original“,返回到reqeust就可能用”final“、”new“修饰,解释到这儿,我想大家对拦截器应该有了一个具象的了解了。
上面我们找到了三个特征,那么接下来我们通过 Jadx 的全局搜索功能,开始寻找某书 Retrofit的拦截器,进而找到 sign 等参数的实现。如果你的电脑内存只有 8G,那么接下来的这一步操作对你十分有用,除此之外,它对我们搜索关键代码点也有奇效。
反编译一个App,你会看到成千上万的类和方法,我们可以简单将这些类和方法分成“具体业务逻辑的代码”和“App架构和工具的代码”,前者的类名一般是包名.xxx,比如某书App包名为 com.xingin.xhs,它的业务代码就是 com.xingin.xxxx;而第二类往往五花八门,比如 Android本身的一些方法类 “android.xx”,腾讯的sdk“com.tencent.xxx”,微博的登录接入sdk“com.weibo.xxx”。
我们想要定位到的内容/加解密逻辑等等,基本都在前者的类里,而后者既缭乱人眼,烦人心神,又占用内存,Jadx给我们提供了屏蔽这些类的方法,屏蔽后Jadx将不再反编译这些类,你也无法再跳转到该方法里,或者在全局检索时看到这些类中扰人的代码。
方法很简单,只需要在类列表中选中某个类,右键 exclude 即可屏蔽这个类。