1.疑问
前些天有朋友问了我一个问题:对于像Http(s)URLConnection这样Android官方提供的用于进行网络连接的类,在进行很好的封装后,使用起来也很方便,但问题来了,使用它们是否足够可靠?
由于发现有一些基于HttpURLConnection的网络请求框架在github上已经收获了很多的star, 可能现在也有不少人在项目中采用了它们,但是在经过深入地查看源码之后发现,这样做是有很大的安全隐患的,所以特意写了本篇文章,希望看到本文的童鞋能够尽快切换到使用最新的okhttp版本。
2.分析
相信很多人都知道,从Android 4.4开始,Http(s)URLConnection的底层实现就使用okhttp了,但是具体是如何实现的呢?
毕竟Http(s)URLConnection对外的接口和okhttp对外的接口不一样,这样的话就必然存在一些类作为中间桥梁,将HttpURLConnection的方法调用转换为对okhttp的调用。
由于HttpsURLConnection和HttpURLConnection的使用大同小异,所以这里就以HttpURLConnection为例进行讲解,另外,由于Android 5.0-Android 7.0中都是基于okhttp 2.x版本的,从而具体的调用差别不大,这里就以Android 5.0的源码为例进行讲解。
2.1 HttpURLConnection的请求过程
HttpURLConnection对外提供的接口确实是很方便使用,一个典型的使用HttpURLConnection进行网络请求的过程如下:
其中蓝色标注的是过程中的重要方法调用,从图中可以看出来,其实无非也就是设置连接参数,写入头部参数,建立连接,获得输出流之后将请求体写入,之后获得服务器的响应流,将响应流再转换为响应实体(比如JSON对象,文件等)。
那么问题来了,在我们调用HttpURLConnection的这些方法时,它是如何将其转换为okhttp的方法调用的?
下面将从okhttp的角度进行详细的拆解。
2.2 结合okhttp进行拆解
在拆解之前,有必要先了解一下源码的结构,需要注意的是HttpURLConnection相关的源码在不同的Android版本上可能会不一样,比如在Android 5.x上是在/libcore/luni/src/main/java/java/net下的,而在Android 7.x上是在/libcore/ojluni/src/main/java/java/net下面。
而okhttp的源码,则一直是在/external/okhttp/下。
架构其实很简单,UML图如下所示:
了解了大致的架构,再去拆解就很简单了。
2.2.1 URL.openConnection()的拆解
首先是URL.openConnection的拆解,该方法如下:
|
|
这个方法非常简单,所以看一下streamHandler对象是什么,看一下setupStreamHandler()方法:
|
|
显然,这里是通过反射调用com.android.okhttp.HttpHandler来新建对象,但关键是:整个Android源码中根本找不到com.android.okhttp.HttpHandler这个类!!!
当时这个问题确实困扰了好一会儿,后面才发现其实是因为jarjar-rules的存在,其实路径为/external/okhttp/jarjar-rules.txt,内容如下:
|
|
这表示com.squareup开关的包会在编译时打包成com.android开头的包,所以这里的com.android.okhttp.HttpHandler对应的源码其实是com.squareup.okhttp.HttpHandler,查找发现这个类是存在的,而它openConnection()方法如下:
|
|
到这里就看到OkHttpClient了,而实际上OkHttpClient中本来没有open()这个方法的,显然这是ASOP团队添加的,方法如下:
|
|
可见,URL.openConnection()方法拆解开来如下:
2.2.2 HttpURLConnection.connect()的拆解
从前面的分析可知,URL.openConnection()最终返回的是HttpURLConnectionImpl对象,所以HttpURLConnection.connect()其实就是HttpURLConnectionImpl.connect(),该方法如下:
这里其实就是初始化HttpEngine和调用execute()方法,该方法如下:
|
|
这里的recover()其实也一样埋下了我在专栏上一篇文章中提到的无限重试的隐患,这个后面会再说。
显然,在这里才真正地发送请求并和服务端建立连接。所以从okhttp的角度来年HttpURLConnection.connect()方法的调用过程如下:
2.2.3 HttpURLConnection.getOutputStream()的拆解
类似地,看HttpURLConnectionImpl的getOutputStream()方法:
|
|
由于连接复用,所以这里其实并不会真的再次创建连接,所以这里其实关键就是httpEngine.getBufferedRequestBody()了,这个方法如下:
|
|
显然是调用了getRequestBody(),而getRequestBody()方法如下:
|
|
这个方法的注释写得很清楚了,就是返回请求体,如果这个请求根本没有body的话就返回null,而requestBodyOut的赋值其实是发生在HttpEngine.sendRequest()中,如下:
|
|
注意其中的requestBodyOut=transport.createRequestBody(request); 而一旦建立了输出流,就可以往里面写入请求体了。所以从okhttp的角度来看,HttpURLConnection.getOutputStream()的调用过程异常简单:
#####2.2.4 HttpURLConnection.getInputStream()的拆解
在往输出流中写入请求体之后,下一步就是获取服务端的响应,即获得输入流。类似地,查看HttpURLConnectionImpl.getInputStream()的实现,如下:
|
|
如果连接正常的话,此时的HttpEngine中已经有了服务端的响应了,下面看HttpEngine.getResponseBodyBytes()方法:
注意其中的getResponseBody()方法:
|
|
而responseBody的赋值在HttpEngine.readResponse()方法中,如下:
|
|
注意其中的initContentStream(),它就是用于初始化输入流的:
|
|
而这个输入流来自于Transport.getTransferStream()方法,如果是http协议的话,则实现类为HttpTransport,该方法如下:
|
|
所以,HttpURLConnection.getInputStream()方法拆解开如下:
3.结论
由前一节的分析可知,从Android 4.4开始,HttpURLConnection的实现确实是通过调用okhttp完成的,而具体的方法则是通过HttpHandler这个桥梁,以及在OkHttpClient, HttpEngine中增加相应的方法来实现,当然,其实还涉及一些类的增加或删除,但是由于不是核心类,这里就不再赘述了。
表面来看,似乎这一切都没什么问题,但是,由于其实现是调用okhttp,那么okhttp有的bug它其实一个也不少(比如HttpEngine中的recover()方法其实一样也会有无限重试的隐患),那我们来看看Android各版本引用的okhttp版本是多少呢!
- Android 4.4.4_r1: 1.1.2
- Android 5.0.1_r1: 2.0.0
- Android 6.0.1_r1: 2.4.0
- Android 7.1.0_r1: 2.6.0
看到Android 7.1.0还在使用okhttp 2.6.0的时候,心中有一万头草泥马呼啸而过。8.0的源码暂时还下载不到,所以暂时还不知道它采用了什么版本,不过几乎可以肯定是没有跟上主流的okhttp版本,所以结论就很明显了:
使用Http(s)URLConnection进行网络请求是极其不可靠的,除非你想把okhttp从1.1.2以来到现在的bug再经历一遍,而且是在不同的手机版本上经历不一样的bug!