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)
}
}
那么这篇文章就到此结束了,很感谢你观看,如果喜欢请点赞关注,如果你有疑问或者有更好的建议请在评论区给我留言,我会尽快回复