写在前面
本人从事Android开发工作几年,也算是见证了Android的技术变革。拿网络库来讲从最开始使用的xUtils到 volley然后到现在主流的okhttp。更不要说热更新、插件化、以及路由开发模式的大行其道。有感于工作中大多时候是仅限于使用,于是打算写一系列关于Android开发中使用到的框架解析,也算是对自己理解的一个记录。
正文
网上看到一个观点:当我们想深入了解一个框架的时候,第一步是要会用,然后按照框架的流程图一步一步的去慢慢的探索分析。对于这个观点笔者是非常赞同的,优秀的框架都是有清晰的架构、核心思想也都采用了优秀的设计模式。下面我们从okhttp开始分析
- 本系列文章基于okhttp4.2.2 (官方已使用kotlin进行重写)
发起一次简单的请求
任何框架的学习第一步都是从会用开始,下面我们开始使用OKHTTP构建一个简单的访问百度的请求。
private fun makeCall() {
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
.url("http://www.baidu.com")
.get()
.build()
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
}
})
}
可以看到我们首先构造一个okhttpClient出来并设置一些超时时间、通过request设置请求的域名和请求方法。最后通过enqueue异步、或者excute同步的方式去进行请求。请求结果会通过接口回调的方式返回。
OkHttpClient
字面意思非常好理解,okhttp的客户端。首先来看一下client的类注释。大概意思如下:
|
使用的时候最好构建一个单例的client去复用到所有的网络请求,因为每个client都会持有它自己的连接池和线程池。复用连接池可以减少延迟并且可以减少内存的消耗,同时通过newBuilder()方法构造出来的client可以很方便的共享一些配置如:connectTimeOut等
Tips:看到上面的注释,网络优化角度为什么要复用连接池也就可以很明白的理解了。 | |
okhttpclient是通过建造者模式构造出来,上面的示例我们发现真正在使用的时候需要我们设置的项并不多,但其实client在构造的时候是有很多参数可以设置。
//异步请求什么时候执行的策略管理器
internal var dispatcher: Dispatcher = Dispatcher()
//管理http请求的连接来降低网络延迟的类
internal var connectionPool: ConnectionPool = ConnectionPool()
//一个网络请求拦截器
internal val interceptors: MutableList<Interceptor> = mutableListOf()
//网络部分拦截器
internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()
//整个网络流程的监听器、我们可以通过设置整个监听来感知网络请求的质量、响应体大小以及持续时长等等。
internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory()
//连接失败时候是否重试
internal var retryOnConnectionFailure = true
//暂时没看懂是干嘛的,默认值是不添加,日常开发中也不会用到
internal var authenticator: Authenticator = Authenticator.NONE
//是否可以重定向
internal var followRedirects = true
//ssl是否可以重定向
internal var followSslRedirects = true
//这个字面意思理解就可以了 一般在请求中需要加入用户的token的时候会用到 默认是没有cookie
internal var cookieJar: CookieJar = CookieJar.NO_COOKIES
//网络请求的缓存策略
internal var cache: Cache? = null
//dns 一个网络请求过程中 会首先把域名解析成ip 这样客户端才知道往什么地址发起请求。
internal var dns: Dns = Dns.SYSTEM
//设置代理
internal var proxy: Proxy? = null
//代理选择器
internal var proxySelector: ProxySelector? = null
//跟上面一样 属于代理ip的一个属性
internal var proxyAuthenticator: Authenticator = Authenticator.NONE
//socket 工厂
internal var socketFactory: SocketFactory = SocketFactory.getDefault()
//https 的socket
internal var sslSocketFactoryOrNull: SSLSocketFactory? = null
//证书信任管理器,一般为了防止https 握手失败我们会设置信任所有的证书
internal var x509TrustManagerOrNull: X509TrustManager? = null
//连接说明 指定socket连接时的一些配置 如:https还是http或者TLS的版本、密码来保证安全的连接
internal var connectionSpecs: List<ConnectionSpec> = DEFAULT_CONNECTION_SPECS
//连接时选择的协议 HTTP_1_0、HTTP_1_1、HTTP_2等
internal var protocols: List<Protocol> = DEFAULT_PROTOCOLS
//域名验证的具体接口实现。如果客户端跟服务端的hostName不一致的时候选择是否接受的策略
internal var hostnameVerifier: HostnameVerifier = OkHostnameVerifier
//按照我的理解是用于信任加入中间代理 如charles这种的代理
internal var certificatePinner: CertificatePinner = CertificatePinner.DEFAULT
//用于计算高效的证书链
internal var certificateChainCleaner: CertificateChainCleaner? = null
//调用超时时间
internal var callTimeout = 0
//连接超时时间 10_000 挺有意思的 是kotlin里面特有的写法
internal var connectTimeout = 10_000
//读数据超时时间
internal var readTimeout = 10_000
//写入流数超时时间
internal var writeTimeout = 10_000
//ping的间隔
internal var pingInterval = 0
从上面可以看出其实okhttp内部帮我们封装了很多可以修改的配置,参数一般有默认值,通过建造者模式我们可以选择性的设置我们想要的配置。okhttp也是我们学习这种设计模式的一个比较好的实践。
构建client之后,我们会通过newCall方法传入一个Request 构造出一个Call.
override fun newCall(request: Request): Call {
return RealCall.newRealCall(this, request, forWebSocket = false)
}
Request
request字面意思就是请求的意思,那么一个请求会包含哪些信息呢?我们进去源码会发现其同样也是通过builder模式构建的。
internal var url: HttpUrl? = null
internal var method: String
internal var headers: Headers.Builder
internal var body: RequestBody? = null
可以发现相对于client,request只有简单的几个参数。
| 参数 | 说明 |
|---|---|
| url | 代表此次请求的链接 |
| method | 代表当前请求的方法。如post、get、delete等 |
| headers | 一次请求中的头文件 |
| body | 请求体 |
Call
我们通过client.newCall()方法构造出来了一个call对象 。实际上是调用了RealCall.newRealCall(this, request, forWebSocket = false).第三个参数是代表不支持webSocket,因为本身是http请求。这点在构建request时HttpUrl类也会有相同的判断,直接把webSocket转换为http请求。具体的使用socket的场景在后续会进行分析。
我们继续往下面看 newCall()方法到底做了什么事情:
fun newRealCall(
client: OkHttpClient,
originalRequest: Request,
forWebSocket: Boolean
): RealCall {
// Safely publish the Call instance to the EventListener.
return RealCall(client, originalRequest, forWebSocket).apply {
transmitter = Transmitter(client, this)
}
}
通过RealCall的构造方法实例出来一个realCall对象,字面意思可以知道后续的请求实际上是调用的realCall的对象。同时初始化了一个Transmitter。
Transmitter
| 应用层和网络层之间的桥梁,对外暴露了像connections/requests/responses/streams 所以我们可以方便的拿到这些对象做操作。再结合拦截器的作用也就很好理解了 |
请求流程
我们知道okhttp是支持同步excute和异步enqueue的,那么内部究竟是怎么处理的呢?Android在某个版本之后已经不允许直接在主线程中进行网络请求了,所以我们这里直接分析异步请求的过程. 我们之前通过client.newCall已经构造出来Call对象。直接使用call.enqueue()就可以发起请求。Call定义的enqueue方法只是一个接口,具体实现是realCall中
override fun enqueue(responseCallback: Callback) {
synchronized(this) {
//首先会进行防止重复请求的判断
check(!executed) { "Already Executed" }
executed = true
}
//追溯进去会发现这步是回调eventListener的callStart方法,前文在介绍okhttpclient的构造方法时有提到过
transmitter.callStart()
//这里是请求真正发起的地方
client.dispatcher.enqueue(AsyncCall(responseCallback))
}
Dispatcher
我们发现真正发起请求的地方是调用client里面的dispathcer对象执行enqueue方法,那dispatcher是什么鬼东东呢?我们继续来看注释:
Policy on when async requests are executed.
用于管理异步请求什么时候执行的策略管理器
Each dispatcher uses an [ExecutorService] to run calls internally. If you supply your own
executor, it should be able to run [the configured maximum][maxRequests] number of calls concurrently.
每个dispatcher本质上是使用ExecutorService来进行请求的调用。
根据上面的注释我们还知道每个dispatcher其实是定义了一个最大请求并发数,默认是64
继续回到我们的主题:
internal fun enqueue(call: AsyncCall) {
synchronized(this) {
readyAsyncCalls.add(call)
// Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
// the same host.
if (!call.get().forWebSocket) {
val existingCall = findExistingCallWithHost(call.host())
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
}
}
promoteAndExecute()
}
首先是使用了同步锁来保证线程同步,然后把当前请求对象添加到了一个ArrayDeque双端队列里。
/** Ready async calls in the order they'll be run. */
private val readyAsyncCalls = ArrayDeque<AsyncCall>()
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private val runningAsyncCalls = ArrayDeque<AsyncCall>()
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private val runningSyncCalls = ArrayDeque<RealCall>()
我们可以看到ok其实是定义了三个双端队列:
- 异步请求等待队列
- 异步请求执行队列
- 同步请求执行队列
首先执行的异步请求必须不是webSocket的类型才继续往下走,下一步是寻找是否存在相同host的请求。这里是通过原子类型来保证相同host的请求可以准确记录下来。接着调用promoteAndExecute()来进行请求;
promoteAndExecute
private fun promoteAndExecute(): Boolean {
//这里的写法很有意思,通过kotlin的assert来进行判断 不满足条件抛出一个异常,后续的开发其实可以借鉴。
assert(!Thread.holdsLock(this))
//构建一个空的AsyncCall list,AsyncCall 其实是自定义的runnable
val executableCalls = mutableListOf<AsyncCall>()
val isRunning: Boolean
synchronized(this) {
val i = readyAsyncCalls.iterator()
while (i.hasNext()) {
val asyncCall = i.next()
if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
if (asyncCall.callsPerHost().get() >= this.maxRequestsPerHost) continue // Host max capacity.
i.remove()
asyncCall.callsPerHost().incrementAndGet()
executableCalls.add(asyncCall)
runningAsyncCalls.add(asyncCall)
}
isRunning = runningCallsCount() > 0
}
for (i in 0 until executableCalls.size) {
val asyncCall = executableCalls[i]
asyncCall.executeOn(executorService)
}
return isRunning
}
大概总结一下这个方法做了什么事情:
-
从上文说的异步请求准备队列中通过迭代的方式取出请求任务
-
判断并发请求是否超过最大数量
-
往计数器中增加一次计数
-
将call对象添加到executableCalls 以及 异步请求执行队列中
-
循环从executableCalls 取出asyncCall对象调用executeOn()方法
executeOn
fun executeOn(executorService: ExecutorService) {
assert(!Thread.holdsLock(client.dispatcher))
var success = false
try {
executorService.execute(this)
success = true
} catch (e: RejectedExecutionException) {
val ioException = InterruptedIOException("executor rejected")
ioException.initCause(e)
transmitter.noMoreExchanges(ioException)
responseCallback.onFailure(this@RealCall, ioException)
} finally {
if (!success) {
client.dispatcher.finished(this) // This call is no longer running!
}
}
}
分析到这里已经很明确了,executeOn方法是通过executorService的execute()方法来执行一次异步请求。如果请求中抛出异常那么会调用dispatcher的finish方法来结束本次请求。
既然是通过异步执行,我们再来看一下run方法
override fun run() {
threadName("OkHttp ${redactedUrl()}") {
var signalledCallback = false
transmitter.timeoutEnter()
try {
val response = getResponseWithInterceptorChain()
signalledCallback = true
responseCallback.onResponse(this@RealCall, response)
} catch (e: IOException) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for ${toLoggableString()}", e)
} else {
responseCallback.onFailure(this@RealCall, e)
}
} finally {
client.dispatcher.finished(this)
}
}
}
代码也同样很精简:
-
通过getResponseWithInterceptorChain()方法来获取请求的响应体(关于这个方法将在后续文章详细分析)
-
通过接口回调将本次请求结果一层层回调给调用类
总结
以上基本上是客户端构建请求、发起请求、以及请求的整体流程。我们可以大概整理一下整个流程做了哪些关键的步骤:
-
客户端通过builder模式构建一个okHttpClient对象
-
客户端通过builder模式构建一个Request对象
-
通过okHttpClient.newCall(request:Request)方法来创建一个Call对象
-
通过call.enqueue(callBack:CallBack)来发起一次异步请求
-
请求时候是通过ExecutorService调用execute()方法来执行
-
最终拿到响应体回调给客户端
当然真正请求的内部流程其实是要复杂的多,上面只是粗略的整理了一下,但是熟悉整个流程对于后面我们进入到okhttp核心代码分析的时候会更加有助于我们理解。以上就是okhttp源码分析的第一篇文章,后面会对每一步进行更详细的分析。