Android OkHttp源码分析(一):为什么OkHttp的请求速度很快?为什么可以高扩展?为什么可以高并发

目录

一、为什么要使用OkHhttp?


在不使用OkHhttp之前,我们都是在使用什么?使用HttpURLConnection,那么我们看看HttpURLConnection发起一次请求,两次请求要花多长时间,而OkHttp花多长时间。HttpURLConnection会比okhttp花更多的时间。

(1)HttpURLConnection

kt 复制代码
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

kt 复制代码
  // 发送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}")
                }
            }
        })
    }
kt 复制代码
 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)。这些队列共同协作,确保网络请求的有序进行。

  1. 线程池:分发器还负责维护一个线程池(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,没有长度,只要你提交一个请求,他就会失败,那么失败以后,就会看是否达到了最大线程数,如果没有,就立马创建一个继续执行。所以他是一个无需等待,最大并发的线程池。高并发。

总结:

  1. 当一个任务通过execute(Runnable)方法添加到线程池时:线程数量小于corePoolSize,新建线程(核心)来处理被添加的任务;
  2. 线程数量大于等于 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请求的发送和响应的接收过程,是实际与服务器进行交互的拦截器。

好了,这篇文章就先介绍到这里。

相关推荐
闲暇部落17 分钟前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX2 小时前
Android 分区相关介绍
android
大白要努力!3 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee3 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood4 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-6 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen9 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年16 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿19 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神20 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri