okhttp官方拦截器之殇
众所周知,当我们使用okhttp并且希望打印网络请求的日志时,我们会在okhttp的拦截器链中添加一个日志拦截器,并且在拦截器中输出网络请求和网络相应的各种信息(包括请求头,请求体,url等)。很暖心的是,官方给我们提供了一个日志拦截器HttpLoggingInterceptor,于是乎你喜闻乐见地把日志拦截器添加到了okhttp的拦截器序列中,一切都没问题,日志输出的很清晰,直到你遇上了下载请求,程序出现阻塞,卡死。。。
问题本源
要说清楚这个问题,我们要先搞清楚所谓的下载请求和普通请求有什么区别,答案是没有区别,他们都是将管道里面的字节流输出到手机中,只不过下载请求的目的地是存储卡,普通请求的目的地是内存(当然普通的请求也是广义上的“下载”,只不过数据一般不是文件而是接口返回的结构化数据)。因此,我们回到okhttp的官方拦截器源码中去看(这里用的是新版的okhttp,已经用kotlin重写)。
class HttpLoggingInterceptor @JvmOverloads constructor( private val logger: Logger = Logger.DEFAULT ) : Interceptor { //...省略部分代码 @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { //...省略部分代码 val contentType = responseBody.contentType() val charset: Charset = contentType?.charset(UTF_8) ?: UTF_8 if (!buffer.isProbablyUtf8()) { logger.log("") logger.log("<-- END HTTP (binary ${buffer.size}-byte body omitted)") return response } if (contentLength != 0L) { logger.log("") //关键,将管道里面的数据写入到字符串中,并用log输出 logger.log(buffer.clone().readString(charset)) } //...省略部分代码 } }
我们可以看到,这个拦截器的本质就是将管道深拷贝一份,然后输出到字符串中(即内存中),这个对于一般的请求而言没有问题,无非最多就是几kb的字符串数据(后台传回来的结构化信息),但是对于下载请求而言是致命的,因为这个拦截器尝试把一个几mb(甚至几百mb)的内容缓存到内存里面!一个普通的安卓app进程也就几百mb,问题出现在这里。
解决方案
解决思想非常简单,由于这个拦截器提供了分级的选项,我们可以想办法让它只有遇到非下载请求的时候,才去使用BODY级,而遇到下载请求的时候,切换到HEADER或者BASIC级让它规避把响应体输出到内存即可,但是真的有那么简单吗?我们继续回到源码。
class HttpLoggingInterceptor @JvmOverloads constructor( private val logger: Logger = Logger.DEFAULT ) : Interceptor { //... }
很遗憾,这是一个不可重写的类,官方也没有提供类似的api让我们持有这种能力,笔者看了网上很多的解决方案,大多数是选择第三方框架或者直接复制粘贴源码然后修改,这两种解决方案笔者都不太满意,因此决定另寻一种出路,这里笔者选择了一种设计模式:代理模式。
顺便推荐一个适合学习设计模式的网站,图文并茂非常适合初学者理解设计模式:免费在线学习代码重构和设计模式 (refactoringguru.cn)
废话不多说直接上代码:
abstract class HttpLoggingProxy( private val client: HttpLoggingInterceptor ) : Interceptor { /** * 通过请求判断是否需要输出日志 */ abstract fun needToLog(request: Request):Boolean override fun intercept(chain: Interceptor.Chain): Response { val request=chain.request() //需要输出日志,用日志拦截器输出 return if(needToLog(request)){ client.intercept(chain) } //不需要输出日志 else{ chain.proceed(request) } } }
笔者设计了一个抽象类,去持有被代理的日志拦截器,然后通过needToLog(request: Request)方法去判断是否要输出日志,如果需要输出日志,则使用被代理的拦截器去拦截chain,否则继续把request往下一个拦截器传递。
笔者注:你可以再传入一个level为BASIC的拦截器,如果不需要输出body的情况则用那个拦截器,这样在下载请求中也可以监听一下请求头,url等信息
然后继承抽象类,把你实现的拦截器代理器传入到okhttp的拦截器链中即可
class MyHttpLoggingProxy:HttpLoggingProxy( HttpLoggingInterceptor() ) { override fun needToLog(request: Request): Boolean { //通过请求来判断是否需要下载 return request.url.toString()=="你喜欢的业务" } }
更进一步:在Retrofit中的使用
我们再实现一个在Retrofit中使用日志拦截器代理器的方法,由于Retrofit在构建okhttp请求的过程中,会把请求方法(即那个Retrofit的Service接口里面的方法)放入到okhttp的Tag中,因此我们可以通过这个Tag来判断方法是否包含了Stream这个注解,如果包含则说明我们在使用Retrofit完成下载的功能。
/** * 返回某个Retrofit定义在方法上的注解,例如[POST],[GET] */ fun <T : Annotation> Request.getMethodAnnotation(annotationClass: Class<T>): T? { return tag(Invocation::class.java)?.method()?.getAnnotation(annotationClass) } fun <T : Annotation> Request.containMethodAnnotation(annotationClass: Class<T>): Boolean { return getMethodAnnotation(annotationClass) != null } class RetrofitHttpLoggingProxy:HttpLoggingProxy( HttpLoggingInterceptor() ) { override fun needToLog(request: Request): Boolean { //Retrofit方法中是否包含了Streaming这个注解,如果包含则说明是下载,不输出日志 return !request.containMethodAnnotation(Streaming::class.java) } }
那么这篇文章就到此结束了,很感谢你观看,如果喜欢请点赞关注,如果你有疑问或者有更好的建议请在评论区给我留言,我会尽快回复