目录
一、为什么要使用OkHhttp?
在不使用OkHhttp之前,我们都是在使用什么?使用HttpURLConnection,那么我们看看HttpURLConnection发起一次请求,两次请求要花多长时间,而OkHttp花多长时间。HttpURLConnection会比okhttp花更多的时间。
(1)HttpURLConnection
fun sendGetRequest(urlString: String): String? {
var response: String? = null
var startTime = System.currentTimeMillis() // 记录开始时间
try {
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
// 设置请求方法为GET
connection.requestMethod = "GET"
// 连接服务器
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
// 读取响应内容
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
val responseBuilder = StringBuilder()
val readLine = reader.readLine()
println("GET请求成功:$readLine")
response = responseBuilder.toString()
} else {
// 处理错误情况
println("GET请求失败: HTTP错误码 $responseCode")
}
} catch (e: Exception) {
e.printStackTrace()
println("GET请求失败: "+e)
}
var endTime = System.currentTimeMillis() // 记录结束时间
val duration = endTime - startTime // 计算总时间
println("请求总时间: $duration 毫秒")
return response
}
(2)OkHttp
// 发送GET请求并记录时间的函数
fun sendGetRequestWithTime(url: String, callback: (String?, Long) -> Unit) {
val client = OkHttpClient()
val request = Request.Builder()
.url(url)
.build()
var startTime = System.currentTimeMillis() // 记录请求开始时间
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
val endTime = System.currentTimeMillis()
val duration = endTime - startTime
callback(null, duration) // 请求失败时回调,传递null作为响应体和持续时间
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
val endTime = System.currentTimeMillis()
val duration = endTime - startTime
if (response.isSuccessful) {
// 读取响应体
response.body?.string()?.let { responseBody ->
callback(responseBody, duration) // 请求成功时回调,传递响应体和持续时间
}
} else {
// 处理HTTP错误
callback(null, duration) // 传递null作为响应体和持续时间
println("HTTP请求失败: ${response.code}")
}
}
})
}
var btnOkhttp: Button = findViewById(R.id.btn_okhttp);
btnOkhttp.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
sendGetRequestWithTime("xxxxx") { responseBody, duration ->
if (responseBody != null) {
println("响应内容: $responseBody")
println("请求总时间: $duration 毫秒")
} else {
println("请求失败或未获取到响应内容")
}
}
}
}
可以看到,时间不相上下,为什么呢?不是说OkHttp更快?其实,从Android4.4开始HttpURLConnection的底层实现采用的是okHttp。
所以如果是使用android4.4以前的版本,就会发现HttpURLConnection比okhttp慢,并且每次请求都是这么慢,比如每次都需要两秒的时间,两次请求,就花了四秒,而Okhttp就不一样,第一次请求建立会花些时间,但随后的请求就是毫秒级,这究竟是为什么?
那么?耗时的地方究竟是在哪里吗?
我们先了解一下Http的请求过程是怎么样的。
二、Http请求的过程是怎么样的
结论:因为他支持一个主机一个长连接,允许对同一主机的所有请求共享一个套接字;
首先,Http协议用来规范我们的格式,负责数据的收发和管理,那么用什么传输呢?就需要TCP来拿建立连接传输,而这个过程需要三次握手,TCP要传输,也要知道往哪里来传递,所以借助IP协议来确认传输的目的地。而TCP/IP的连接,在程序里面我们就可以使用Socket来建立。建立后,要完成Http的通讯,那么我们就需要建立Http的报文,使用Socket的流来发送数据。
在这整一个过程中,如果说一次请求要花两秒,那么1.8秒都花在了建立三次握手这里,而数据传输0.2秒。
所以OkHttp就对这一个过程做了优化,对域名建立长连接,数据传输共享一个套接字,后续数据传输的时候,就不需要建立三次握手了。
并且Okhttp提供默认的请求压缩格式Gzip,能够将数据进行压缩,这样就可以提高我们传输的效率。
Socket和TCP/IP有什么关系?Socket是他们的抽象,也可以说是上层实现,封装。
三、分发器是什么?
通过上面的代码,我们可以看到,OkHttp请求过程中最少只需要接触OkHttpClient、 Request、 Call、Response
大量的逻辑处理被封装了,所有的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。
那么分发器是做什么的?主要是负责我们请求任务什么时候执行,什么时候被调用。注意他只是做请求任务的调配和分发,还没开始发送请求,因为那是拦截器做的事情:
1.请求队列:分发器内部维护了多个请求队列,包括等待执行的异步任务队列(readyAsyncCalls)、正在运行的异步任务队列(runningAsyncCalls)以及运行中的同步任务队列(runningSyncCalls)。这些队列共同协作,确保网络请求的有序进行。
2. 线程池:分发器还负责维护一个线程池(ThreadPoolExecutor),用于执行异步网络请求。线程池可以避免频繁地创建和销毁线程,从而提高程序的性能和效率。
3.1 源码分析:请求队列
可以看到我们会传递request进去,Request对象用来表示你想要进行的HTTP请求(包括URL、请求头、请求体等)
Call,就是一个任务。这个Call对象封装了实际的请求操作,并提供了方法来执行请求、取消请求以及监听请求结果。调用enqueue方法,这个方法会异步地执行HTTP请求。异步,那么就是线程。
enqueue方法只能调用一次,否则就会报错。
接下来我们看看最后一句代码:client.dispatcher.enqueue(AsyncCall(responseCallback)),Dispatcher类,我们可以看到里面有三个队列:等待执行的异步任务队列(readyAsyncCalls)、正在运行的异步任务队列(runningAsyncCalls)以及运行中的同步任务队列(runningSyncCalls),我们调用的是enqueue,所以会往异步队列里面增加:
那么,什么时候往执行队列里面增加,什么时候完准备队列里面增加呢?正在执行的异步个数,大于64,则放到准备队列。同个域名的请求最大个数不大于5个,总要有一个上限,不能无限制。
将符合条件的调用从readyAsyncCalls提升到runningAsyncCalls
请求执行完成后,就会继续取下一个
3.2 源码分析:线程池
为什么他要这样定义?这些参数的作用是什么?
核心线程数:定义为0,比如你设置成了x个,那么就会一直维护x个线程。
最大线程数:同时执行的最大线程数量
空闲时间:空闲了60s的超过核心线程数的线程会被回收
任务队列:SynchronousQueue
为什么使用SynchronousQueue,不使用ArrayBlockingQueue?如果使用ArrayBlockingQueue你需要定义究竟有多少个任务,所以不方便。而SynchronousQueue是一个没有容量的queue,没有长度,只要你提交一个请求,他就会失败,那么失败以后,就会看是否达到了最大线程数,如果没有,就立马创建一个继续执行。所以他是一个无需等待,最大并发的线程池。高并发。
总结:
- 当一个任务通过execute(Runnable)方法添加到线程池时:线程数量小于corePoolSize,新建线程(核心)来处理被添加的任务;
- 线程数量大于等于 corePoolSize,新任务被添加到等待队列,添加失败:线程数量小于maximumPoolSize,新建线程执行新任务;线程数量等于maximumPoolSize,使用RejectedExecutionHandler拒绝策略。
四、拦截器
OkHttp的拦截器(Interceptor)是OkHttp库中的一个核心组件,**负责完成整个请求过程。**它提供了对HTTP请求和响应进行全面控制的能力。拦截器允许开发者在HTTP请求和响应的各个阶段进行自定义处理,比如日志记录、请求重试、响应缓存等。
OkHttp拦截器的执行流程以及拦截器有哪些?
这些拦截器做了些什么样的事情?简单来说,就是打开Socket,然后处理Socket的数据封装成response返回给我们。
4.1 重试和重定向拦截器(RetryAndFollowUpInterceptor):负责重试和重定向
第一个接触到请求,最后接触到响应。
负责判断是否需要重新发起整个请求,包括处理异常和重定向。
在请求阶段发生RouteException或IOException时,会根据一定的逻辑判断是否重新发起请求。
重定向的判断则基于响应码,如果需要重定向,则会构造一个新的请求并继续处理。
4.2 桥接拦截器(BridgeInterceptor):负责补全请求头等报文
补全请求,包括添加必要的请求头信息。
对响应进行额外处理,如处理gzip压缩的响应数据,以及保存和读取cookie。
桥接拦截器在应用层和网络层之间起到了桥梁的作用,确保请求和响应能够正确地被处理和传递。
4.3 缓存拦截器(CacheInterceptor):
请求前查询缓存,如果缓存中存在有效响应,则直接返回缓存响应,避免不必要的网络请求。
如果网络请求成功,则根据缓存策略判断是否需要更新缓存。
缓存拦截器通过管理缓存数据,提高了应用的响应速度和性能。
4.4 连接拦截器(ConnectInterceptor):长连接
负责与服务器完成TCP连接(Socket)。
内部维护一个连接池,负责连接复用、创建连接、释放连接等。
连接拦截器还负责封装请求数据和解析响应数据,如HTTP报文的封装和解析。
4.5 请求服务拦截器(CallServerInterceptor):利用socket发出请求,读取响应
也可以称为“读写拦截器”,因为它主要负责与服务器进行通信,包括发送请求数据和接收响应数据。
封装了HTTP请求的发送和响应的接收过程,是实际与服务器进行交互的拦截器。
好了,这篇文章就先介绍到这里。