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请求的发送和响应的接收过程,是实际与服务器进行交互的拦截器。

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

相关推荐
三少爷的鞋8 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)11 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码13 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓18 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer