前言
看到这个标题,好长哇(恩,好长)!分解一下:
- 使用原生的HttpUrlConnection请求代码.
- 在不改变现有代码的前提下将请求代理到第三方网络库,如OkHttp, Chromium网络栈, CURL等.
- 代理到第三方网络库后可以支持Http/2.0, HttpDNS等特性.
要达到怎么样的一个目的呢,无代码无fuck
1 | try { |
最终的目的就是使用以上代码,但是真正的请求是第三方网络库代理发出去的。
坑爹的Google爸爸
在Android 4.4之后,Google爸爸将Android上的HttpUrlConnection实现修改成了OkHttp,但是这个支持显得有点坑爹,它就是一个黑盒子,没有将任何控制OkHttp的属性暴露出来,其实这才是合理的,毕竟不能让开发者感知到底层的实现嘛。但是翻看AOSP的实现 HttpsHandler.java,会看到一段坑爹的代码
1 | private static final List<Protocol> HTTP_1_1_ONLY = |
看到okHttpClient.setProtocols(HTTP_1_1_ONLY);这一行,没错,对OkHttp设置了只支持Http/1.1协议,即使OkHttp支持了SPDY和Http/2.0,但是也被Google爸爸禁用了。恩,所以别以为底层替换成了OkHttp的实现,就以为OkHttp的所有特性都被继承过来了,这是大错特错的。
细读java.net.URL类
于是我们再去看看java.net.URL类的实现,找一找有没有其他方式,将系统级请求代理到第三方网络库上。
AOSP上的代码在URL.java
URL.openConnection()
首先看到openConnection方法的实现
1 | public URLConnection openConnection() throws java.io.IOException { |
可以看到转交给了handler对象的openConnection,入参是this,也就是URL对象。
那么这个handler是什么东西呢,继续往下看
URLStreamHandler
handler其实就是URLStreamHandler对象
在URL中查找一下代码,可以看到它的构造函数中有这么一段获取handler的代码
1 | if (handler == null && |
从代码中可以看到,handler就是处理某个协议的真正幕后操纵者,接着到了getURLStreamHandler方法,方法的入参就是协议,如http, http, ftp等,如果返回值是空,则当前请求会抛出一个未知协议的异常。
getURLStreamHandler
来看看getURLStreamHandler的真正实现部分
1 | /** |
- 首先从handlers中根据协议返回一个URLStreamHandler对象,handlers是一个静态的Hashtable<String,URLStreamHandler>,主要起到一个缓存的作用,如果获取不到,则继续下一步操作。
- 判断factory对象是否为空,如果不为空,则调用factory.createURLStreamHandler方法获取一个URLStreamHandler对象,如果URLStreamHandler对象不为空,则标记checkedWithFactory变量为true,用于后面检查时使用,如果返回空,则继续下一步操作
- 获取系统java.protocol.handler.pkgs属性,该值是JVM的启动参数,通过-D java.protocol.handler.pkgs来设置URLStreamHandler实现类的包路径,例如-D java.protocol.handler.pkgs=com.sample.protocol,代表处理实现类皆在这个包下。如果需要多个包的话,那么使用“|” 分割。比如-D java.protocol.handler.pkgs=com.sample.protocol1|com.sample.protocol2;而JDK内部默认实现类均是在sun.net.www.protocol包下。设置进去的包下的类的命名模式必须为[package_path].[protocol].Handler,比如我实现了http协议,则对应的实现类为com.sample.protocol.http.Handler,再比如我实现了https协议,则对应的实现类为com.sample.protocol.https.Handler,因为是需要用到反射,所以这些实现类必须有一个默认的构造函数。了解了这个原理之后,之后就是遍历满足条件的所有URLStreamHandler,直到找到一个对应协议的URLStreamHandler,反射构造它;如果找不到,则继续下一步操作。
- 如果协议是file协议,则使用默认包下的package_path.file.Handler对象,即sun.net.www.protocol.file.Handler对象
- 如果协议是ftp协议,则使用默认包下的package_path.ftp.Handler对象,即sun.net.www.protocol.ftp.Handler对象
- 如果协议是jar协议,则使用默认包下的package_path.jar.Handler对象,即sun.net.www.protocol.jar.Handler对象
- 如果协议是http协议,则使用com.android.okhttp.HttpHandler,注意此时OkHttp登场了,调用的方式是反射调用。对应的实现类在HttpsHandler.java
- 如果协议是https协议,则使用com.android.okhttp.HttpsHandler,也是反射调用,对应的实现类在HttpsHandler.java
- 细心的你会发现,代码中反射的是com.android.okhttp.HttpHandler和com.android.okhttp.HttpsHandler,但是AOSP上的源码却是com.squareup.okhttp.HttpHandler和com.android.okhttp.HttpsHandler,这是为什么呢,因为项目目录下存在一个叫jarjar-rules.txt的文件,它会将com.squareup重命名为com.android,以及将okio重命名为com.android.okhttp.okio
- 最后就是一个检查的过程,检查其他线程是不是创建了相关的类,首先从handlers缓存中查找,如果找到了,则直接返回,无论当前的hander是否已经创建,都直接丢弃当前的handler对象,如果找不到,则检查其他线程是不是创建了factory对象,这个前提条件是最开始时factory并没有被创建,从而避免重复创建handler。如果这时候检查的handler2不为空,则将其赋值给handler,并且将handler对象存入handlers缓存中,将hanler对象返回。
从以上代码可以很快的找到我们有两个切入点
- 我们是否可以从构造函数中传入URLStreamHandler对象,代理所有请求
- 我们是否可以全局代理掉URLStreamHandler的创建
对于第一个问题,查找URL构造函数可以发现,确实存在这么一个构造函数,而且还不止一个
1 | public URL(String protocol, String host, int port, String file, |
但是这已经违背了我们不修改现有代码的原则了,因为我们需要修改构造URL对象的代码了,所以我们放弃它。
那么问题就到了第二个上,是否可以全局代理掉URLStreamHandler的创建,经过刚才的一番分析,不难发现,确实是有那么一个角色,负责全局代理URLStreamHandler的创建,并且其优先级是相当的高的。没错,就是factory对象及其createURLStreamHandler方法
URLStreamHandlerFactory
那么factory对象是怎么来的呢,它其实是一个静态变量
1 | static URLStreamHandlerFactory factory; |
默认为空,也就是说如果我们不设置,它就会一直为空,这不就是专门用于自定义用的吗!看下它的设置方法
1 | public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) { |
- 首先会判断factory是不是null,如果不为null,则会抛出一个Error,从这里看出,factory最多仅且可以设置一次
- 调用handlers的clear方法,清空之前缓存的所有handler对象,这个目的是啥呢?当然是为了让设置的factory生效啦,毕竟它是高优先级的,因为可能在设置它之前,已经有一些handler被创建了,让那些已经创建的handler失效。
全局代理请求到OkHttp
okhttp有对httpurlconnection的支持模块,不过其已经被square废弃了,也就是将来不再维护了。不过我们还是可以使用的
引入okhttp及okhttp-httpurlconnection的模块
1 | compile 'com.squareup.okhttp3:okhttp:3.8.1' |
设置URLStreamHandlerFactory对象为OkUrlFactory
1 | try { |
这时候你的请求就被代理到okhttp上了。
如何让OkHttp支持Http/2.0
不用设置,默认5.0以上支持,只要后端服务器支持alpn选择协议,它就能支持。
如何让OkHttp支持HttpDNS
使用OkHttp的Dns接口即可
1 | OkHttpClient client = new OkHttpClient.Builder() |
坑点
这种方式的OkHttp代理,切记不要使用任何拦截器,因为设置了也没有用,在OkHttpURLConnection.java中有个buildCall()方法,负责创建OkHttp的Call对象,在该方法中,会将我们设置进去的client的拦截器全部清空,如下代码
1 | private Call buildCall() throws IOException { |
所以将OkHttp的OkUrlFactory设置为URLStreamHandlerFactory时,设置的OkHttpClient不要添加任何拦截器即可,添加了也会失效,或许这就是Square比较坑的地方,和Google爸爸一样坑。
chromium网络栈的代理
如果你的项目引用了chromium的网络栈,那么也是支持全局代理HttpUrlConnection的请求的,因为chromium也是有URLStreamHandlerFactory模块的支持,参考 Chromium 网络栈的编译与使用
引入cronet依赖
1 | compile 'io.github.lizhangqu:cronet:0.0.1' |
设置URLStreamHandlerFactory对象为CronetURLStreamHandlerFactory
1 | CronetEngine.Builder builder = new CronetEngine.Builder(context); |
如上代码,已经开起了http/2.0及httpdns的支持
总结
此处没有总结!坑已挖,少年快去填坑吧!
