OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用
目录
一,分发器和拦截器
二,分发器处理异步请求
1.分发器处理入口
2.分发器工作流程
3.分发器中的线程池设计
三,分发器处理同步请求
四,拦截器处理请求
1.责任链设计模式
2.拦截器工作原理
3.OkHttp五大拦截器
一,分发器和拦截器
OkHttp在内部维护了这几个重要对象:分发器dispatcher,连接池connectionPool,拦截器Interceptor;
//拦截器
@get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher//连接池
@get:JvmName("connectionPool") val connectionPool: ConnectionPool = builder.connectionPool//拦截器
@get:JvmName("interceptors") val interceptors: List<Interceptor> =builder.interceptors.toImmutableList()@get:JvmName("networkInterceptors") val networkInterceptors: List<Interceptor> =builder.networkInterceptors.toImmutableList()
他们的作用分别为:
- 分发器Dispatcher:调配请求任务,内部维护队列和线程池
- 拦截器:处理请求与响应,完成请求过程
- 连接池:管理socket连接与连接复用
从OkHttp的请求处理流程来看: 拦截器负责完成网络请求过程,同步和异步请求必须经过分发器调配后才会发给拦截器进行网络请求;
二,分发器处理异步请求
1.分发器处理入口
private void visitInternet() {//1.创建HttpClient对象OkHttpClient okHttpClient = new OkHttpClient();//2.获取request对象Request.Builder builder = new Request.Builder().url("https://www.bilibili.com/");Request request = builder.build();//3.获取call对象Call call = okHttpClient.newCall(request);//4.执行网络操作try {Response response = call.execute();String result = response.body().string();showResultOnUiThread(result);} catch (IOException e) {throw new RuntimeException(e);}
}
从OkHttp处理流程来看,每次发送请求前我们需要调用 newCall() 方法获取call对象,这里的Call是一个接口,newCall返回的是Call接口的实现类RealCall;
/** Prepares the [request] to be executed at some point in the future. */override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)
call对象只能使用一次
发起异步请求需要调用call对象的 enqueue() 方法,enqueue方法首先会将call对象中的executed字段置为true,代表这个call对象已经使用过,第二次就无法使用,想要再次使用的话需要调用call对象的 clone() 方法;
callStart方法执行后表示请求开始,之后便会执行分发器的enqueue方法处理异步请求,这里传入的对象AsyncCall是Runnable接口的实现类,可以理解为是我们要处理的异步任务;
override fun enqueue(responseCallback: Callback) {//call对象只能使用一次check(executed.compareAndSet(false, true)) { "Already Executed" }callStart() //请求开始//分发器处理异步请求client.dispatcher.enqueue(AsyncCall(responseCallback))}
2.分发器工作流程
分发器中维护了三个队列:
- readyAsyncCalls:等待中异步请求队列
- runningAsyncCalls:执行中异步请求队列
- runningSyncCalls:执行中同步请求队列
分发器dispatcher的enqueue方法执行后,异步请求AsyncCall默认先放到readAsyncCalls中,如果是非websocket连接,则检查一下runningAsyncCalls和readAsyncCalls中是否有相同域名host的请求,如果有则复用之前的域名的计数器existingCall
计数器之后用于判断同一主机(域名)请求连接数
internal fun enqueue(call: AsyncCall) {synchronized(this) {readyAsyncCalls.add(call)if (!call.call.forWebSocket) {val existingCall = findExistingCallWithHost(call.host)if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)}}promoteAndExecute()}
检查完之后调用promoteAndExecute()方法,在这个方法中会检查两件事:
- 进行中异步请求数是否 ≥ 64(runningAsyncCalls队列的size是否 ≥ 64),
- 对同一域名(主机)的请求callsPerHost是否大于5;
若条件符合,将异步任务加入到runningAsyncCalls中
检查完可执行请求并更新状态后,将请求提交到线程池中执行
private fun promoteAndExecute(): Boolean {this.assertThreadDoesntHoldLock()val executableCalls = mutableListOf<AsyncCall>()val isRunning: Booleansynchronized(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}
将AsyncCall提交到线程池后,AsyncCall对象的run方法便会被执行;
在run方法中,从拦截器中获取了服务器的响应,完成请求后调用dispatcher的finish方法,结束本次异步请求;
override fun run() {threadName("OkHttp ${redactedUrl()}") {var signalledCallback = falsetimeout.enter()try {//拦截器完成请求,返回响应val response = getResponseWithInterceptorChain()signalledCallback = trueresponseCallback.onResponse(this@RealCall, response)} catch (e: IOException) {if (signalledCallback) {// Do not signal the callback twice!Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)} else {responseCallback.onFailure(this@RealCall, e)}} catch (t: Throwable) {cancel()if (!signalledCallback) {val canceledException = IOException("canceled due to $t")canceledException.addSuppressed(t)responseCallback.onFailure(this@RealCall, canceledException)}throw t} finally {//调用finish方法,结束本次异步请求client.dispatcher.finished(this)}}}
在完成一次请求后,runningAsyncCalls队列会空出位置
所以在finish方法中,会重新调用检查异步任务方法promoteAndExecute(),也就是在结束一次请求后,会去检查readyAsyncCalls队列中符合条件的异步任务,并去执行他们
idleCallback.run() 用于在所有请求完成后执行特定操作,操作内容自定义
internal fun finished(call: AsyncCall) {call.callsPerHost.decrementAndGet()finished(runningAsyncCalls, call)}/** Used by [Call.execute] to signal completion. */internal fun finished(call: RealCall) {finished(runningSyncCalls, call)}private fun <T> finished(calls: Deque<T>, call: T) {val idleCallback: Runnable?synchronized(this) {if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")idleCallback = this.idleCallback}//重新调用promoteAndExecute,检查可执行异步请求val isRunning = promoteAndExecute()if (!isRunning && idleCallback != null) {//用于在所有请求完成后执行特定操作,操作内容自定义idleCallback.run()}}
3.分发器中的线程池设计
分发器中的线程池:
- 核心线程数:0
- 最大线程数:Int.MAX_VALUE
- 空闲时间:60s
- 工作队列:SynchronousQueue()
@get:Synchronized@get:JvmName("executorService") val executorService: ExecutorServiceget() {if (executorServiceOrNull == null) {executorServiceOrNull = ThreadPoolExecutor(0, //核心线程数Int.MAX_VALUE, //最大线程数60, //空闲时间TimeUnit.SECONDS, //空闲时间单位(秒)SynchronousQueue(), //工作队列threadFactory("$okHttpName Dispatcher", false))}return executorServiceOrNull!!}
线程池工作原理:
- 工作中线程 < 核心线程数 创建新线程
- 工作中线程 > 核心线程数且工作队列未满,加入工作队列
- 工作队列已满,工作中线程数若 < 最大线程数, 创建新线程
- 工作队列已满,工作中线程数 > 最大线程数, 执行拒绝策略(默认为抛出异常,可自定义)
在okhttp的分发器中,线程池使用SynchronousQueue()作为工作队列,这种容器没有容量,也就无法添加任务,所以当工作中线程 > 核心线程数,会直接创建新线程
三,分发器处理同步请求
对于同步请求,分发器只记录请求(放入RunningSyncCalls中)
override fun execute(): Response {check(executed.compareAndSet(false, true)) { "Already Executed" }timeout.enter()callStart()try {client.dispatcher.executed(this)return getResponseWithInterceptorChain()} finally {client.dispatcher.finished(this)}}//dispatcher.executed()@Synchronized internal fun executed(call: RealCall) {//分发器只记录同步请求runningSyncCalls.add(call)}
四,拦截器处理请求
1.责任链设计模式
OkHttp中的拦截器采用责任链设计模式:
为避免请求发送者与多个请求处理者耦合在一起,于是将所有请求处理者通过前一对象记住下一对象的引用而形成一条链,当有请求发生时,请求只需沿着链传递,直到有对象处理它
模拟责任链设计模式:
我们定义一个Handler抽象类,并让他持有下一Handler对象的引用next,并创建Handler三个子类
abstract class Handler {protected var next : Handler? = null;fun setNext(next : Handler){this.next = next;}fun getNext() : Handler?{return next;}abstract fun handle(request : String);
}class Handler1 : Handler() {override fun handle(request: String) {if("1".equals(request)){Log.i("TAG", "handle1处理")}else{if(getNext() != null){next?.handle(request);}else{Log.i("TAG", "没有下一个handler")}}}
}class Handler2 : Handler() {override fun handle(request: String) {if("2".equals(request)){Log.i("TAG", "handle1处理")}else{if(getNext() != null){next?.handle(request);}else{Log.i("TAG", "没有下一个handler")}}}
}class Handler3 : Handler() {override fun handle(request: String) {if("3".equals(request)){Log.i("TAG", "handle1处理")}else{if(getNext() != null){next?.handle(request);}else{Log.i("TAG", "没有下一个handler")}}}
}
我们让handler1拥有2的引用,2拥有3的引用,这样当我们调用1的handle("3")时,request对象就会一直沿着责任链执行,直到遇到能处理他的对象(handler3)
val handler1: Handler = Handler1()
val handler2: Handler = Handler2()
val handler3: Handler = Handler3()handler1.setNext(handler2)
handler2.setNext(handler3)handler1.handle("3")
2.拦截器工作原理
拦截器的工作基本分为三步:
- 处理请求request
- 将请求传往下一拦截器,获取返回的请求response
- 处理响应response并返回
例如,我们自定义一个日志打印拦截器:
class LogInterceptor : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {//1.处理请求val request = chain.request();val requestLog = StringBuilder().apply {append("Request:\n")append("URL: ${request.url}\n")append("Method: ${request.method}\n")append("Headers: ${request.headers}\n")request.body?.let {append("Body: ${it.toString()}\n")}}Log.d("OkHttp", requestLog.toString())//将请求传往下一拦截器,获取响应val response = chain.proceed(request)//处理响应并返回val responseLog = StringBuilder().apply {append("Response:\n")append("Code: ${response.code}\n")append("Headers: ${response.headers}\n")response.body?.let {append("Body: ${it.string()}\n")}}Log.d("OkHttp", responseLog.toString())return response;}
}
在chain的proceed方法中,程序会找到拦截器链中的下一拦截器并将请求传给他,获取返回的请求
@Throws(IOException::class)override fun proceed(request: Request): Response {check(index < interceptors.size)calls++if (exchange != null) {check(exchange.finder.sameHostAndPort(request.url)) {"network interceptor ${interceptors[index - 1]} must retain the same host and port"}check(calls == 1) {"network interceptor ${interceptors[index - 1]} must call proceed() exactly once"}}// 找到拦截器链中的下一拦截器val next = copy(index = index + 1, request = request)val interceptor = interceptors[index]//传递请求,获取响应@Suppress("USELESS_ELVIS")val response = interceptor.intercept(next) ?: throw NullPointerException("interceptor $interceptor returned null")if (exchange != null) {check(index + 1 >= interceptors.size || next.calls == 1) {"network interceptor $interceptor must call proceed() exactly once"}}check(response.body != null) { "interceptor $interceptor returned a response with no body" }return response}
3.OkHttp五大拦截器
OkHttp中默认配置五个拦截器,分别为:
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {interceptors += client.networkInterceptors
}
nterceptors += CallServerInterceptor(forWebSocket)
- 重试和重定向拦截器 RetryAndFollowUpInterceptor:重试拦截器在交出前(交给下一个拦截器),负责判断用户是否取消了请求。在获得了响应之后,会根据响应码判断是否需要重定向,如果满足所有条件就会重启执行所有拦截器
- 桥接拦截器(处理请求头和响应头)BridgeInterceptor:在交出之前,负责将Http协议必备的请求头加入请求之中(如Host,Connection),并添加一些默认的行为(如RZIP压缩);获得响应后调用保存cookie接口并解析GZIP数据
- 缓存拦截器 CacheInterceptor:交出之前读取并判断是否使用缓存;获取响应后判断是否缓存
- 连接拦截器 ConnectInterceptor:交出之前,负责创建或找到一个连接,并获取socket流;获取响应后不进行额外处理
- 网络请求拦截器(执行实际的网络请求)CallServerInterceptor:进行真正的与服务器通信,向服务器发送数据,解析读取的响应数据
OkHttp中添加拦截器有两种方式:addInterceptor()和 addNetworkInterceptor(),他们的主要区别如下:
- 调用时机:Application拦截器在请求开始时调用,Network在网络连接建立后调用
- 调用次数:Application只调用一次,Network可能调用多次(重定向)
- 可见信息:Application只能看到最终请求/响应,Network能看到所有中间请求/响应
- 缓存感知:Application无法感知缓存,Network可以感知缓存
- 使用场景:Application一般用于业务处理(如:身份验证,日志记录,错误处理),Network一般用于网络层操作(如:网络监控,缓存处理,压缩处理)
OkHttp完整拦截器链如下:
相关文章:

OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用
目录 一,分发器和拦截器 二,分发器处理异步请求 1.分发器处理入口 2.分发器工作流程 3.分发器中的线程池设计 三,分发器处理同步请求 四,拦截器处理请求 1.责任链设计模式 2.拦截器工作原理 3.OkHttp五大拦截器 一&#…...

中国计算机学会计算机视觉专委会携手合合信息举办企业交流活动,为AI安全治理打开“新思路”
近期,《咬文嚼字》杂志发布了2024年度十大流行语,“智能向善”位列其中,过去一年时间里,深度伪造、AI诈骗等话题屡次登上热搜,AI技术“野蛮生长”引发公众担忧。今年9月,全国网络安全标准化技术委员会发布了…...

重生之我在异世界学编程之C语言:深入预处理篇(上)
大家好,这里是小编的博客频道 小编的博客:就爱学编程 很高兴在CSDN这个大家庭与大家相识,希望能在这里与大家共同进步,共同收获更好的自己!!! 本文目录 引言正文一、预处理的作用与流程…...

dolphinscheduler服务RPC框架源码解析(二)RPC核心注解@RpcService和@RpcMethod设计实现
1.工程目录 从3.2.1版本之后这个dolphinscheduler中的RPC框架工程就从原来的dolphinscheduler-remote工程重构到了dolphinscheduler-extract工程。 dolphinscheduler 父项目 dolphinscheduler-extract RPC服务项目 dolphinscheduler-extract-alert 监控告警服务RPC接口定义、…...

【从零开始入门unity游戏开发之——C#篇04】栈(Stack)和堆(Heap),值类型和引用类型,以及特殊的引用类型string
文章目录 知识回顾一、栈(Stack)和堆(Heap)1、什么是栈和堆2、为什么要分栈和堆3、栈和堆的区别栈堆 4、总结 二、值类型和引用类型1、那么值类型和引用类型到底有什么区别呢?值类型引用类型 2、总结 三、特殊的引用类…...

ARCGIS国土超级工具集1.2更新说明
ARCGIS国土超级工具集V1.2版本,功能已增加至47 个。在V1.1的基础上修复了若干使用时发现的BUG,新增了"矢量分割工具"菜单,同时增加及更新了了若干功能,新工具使用说明如下: 一、勘测定界工具栏更新界址点成果…...

暂停window11自动更新
window11 的自动更新功能,一方面在后台占用资源,容易导致电脑卡顿;另一方面,“更新并关机” 和 “更新并重启” 的设置令人极其反感。很多补丁兼容性很差,更新后极易引发电脑蓝屏、闪屏等意想不到的 bug。 1.winR打开运…...

Git简介和特点
目录 一、Git简介 二、Git特点 1.集中式和分布式 (1)集中式版本控制系统 (2)分布式版本控制系统 2.版本存储方式的差异 (1)直接记录快照,而非差异比较 3.近乎所有操作都是本地执行 一、Git简介 Git是目前世界上最先进的的分布式控制系统(没有之一…...

如何通过docker 部署minio,端口号为9105
通过Docker部署MinIO对象存储服务,并指定API端口为9105,可以按照以下步骤进行。我们将基于已有的资料来详细说明这一过程。 1. 准备工作 首先,确保你的系统上已经安装了Docker。如果没有安装,可以根据官方文档指导完成安装。接下…...

设置Qt程序开机自启动(windows版本)
前言 本文展示在windows环境下,通过代码实现更改系统注册表的方式来实现程序的开机自动启动。 一、注册表 需要更改的系统注册表为: HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run 二、代码演示 1.头文件 头文件autorun.h #ifndef …...

【HarmonyOS】鸿蒙获取appIdentifier,Identifier
【HarmonyOS】鸿蒙获取appIdentifier,Identifier 一、前言 三方后台需要填写的所谓appIdentifier,Identifier信息,其实对应鸿蒙应用的appID。 二、解决方案: 注意,模拟器获取data.signatureInfo.appIndentifer为空…...

【Rust自学】3.5. 控制流:if else
3.5.0. 写在正文之前 欢迎来到Rust自学的第三章,一共有6个小节,分别是: 变量与可变性数据类型:标量类型数据类型:复合类型函数和注释控制流:if else(本文)控制流:循环 通过第二章…...

美国信息学奥林匹克竞赛USACO 2024年12月比赛铜级问题1. 循环舍入-答案代码
题目见: USACO 2024年12月比赛铜级问题1. 循环舍入(USACO 2024 December Contest, Bronze Problem 1. Roundabount Rounding) 最简单的青铜级 亲测所有得分点通过哈! 下一篇会给解题分析哦! #include <iostream> using namespace std;…...

Llama3模型详解 - Meta最新开源大模型全面解析
📚 Meta最新发布的Llama3模型在开源社区引起广泛关注。本文将全面解析Llama3的技术特点、部署要求和应用场景。 一、模型概述 1. 基本信息 发布机构: Meta AI开源协议: Llama 2 Community License Agreement模型规格: 7B/13B/34B/70B训练数据: 2万亿tokens上下文长…...

2021-02-12 c++里面cin.sync()函数的意思
回复急~救救菜鸡吧,C用cin.clear()和cin.sycn()清空缓存区一直清不了!_编程语言-CSDN问答 标识符号为:goodbit 无错误 Eofbit 已到达文件尾 failbit 非致命的输入/输出错误可挽回 badbit 致命的输入/输出错误无法挽回 int a 0;cin >> a;if (cin.rdstate() i…...

下载红米Note 9 Pro5G对应的LineageOS代码下载及编译
构建 LineageOS 进入网站:Info about gauguin | LineageOS Wiki,点击:Build for yourself,里面有详细的教程,我这里就按照Note 9 Pro 5G来。 机器环境 Ubuntu环境为:20.04.6LinagesOS版本:21-…...

《探索 Caffe2 的 C++接口在移动设备上的性能优化之路》
在当今移动应用日益智能化的时代,将深度学习框架如 Caffe2 的 C接口应用于移动设备上已成为众多开发者的目标。然而,移动设备资源相对有限,如何优化其性能成为了关键挑战。 一、移动设备应用深度学习的现状与挑战 随着智能手机等移动设备的…...

1.编写一个程序,给定一个大写字母,要求用小写输出
思路:ascII码值 例如:A的ASCII码值为65,a的ASCII码值为97,不难发现,大小写字母之间的ascii值相差了32 #include <stdio.h> int main() {char c;scanf("%c",&c);printf("%c",c32);retu…...

条件随机场(CRF)详解:原理、算法与实现(深入浅出)
目录 1. 引言2. 什么是条件随机场?2.1 直观理解2.2 形式化定义 3. CRF的核心要素3.1 特征函数3.2 参数学习 4. 实战案例:命名实体识别5. CRF vs HMM6. CRF的优化与改进6.1 特征选择6.2 正则化 7. 总结与展望参考资料 1. 引言 条件随机场(Conditional Ra…...

Android Studio、JDK、AGP、Gradle、kotlin-gradle-plugin 兼容性问题
文章目录 问题:解决办法:gradle与 java的版本兼容AGP与Gradle的版本兼容kotlin 与 jvm 的版本兼容KGP、Gradle、AGP兼容关系kotlin 与 java 的编译版本配置 问题: 你从githb上clone了一个项目,本地跑的时候,各种报错。…...

防抖(Debounce)和节流(Throttle)的区别和应用场景
防抖(Debounce)和节流(Throttle)虽然都是用来限制函数的执行频率,但它们的实现方式和应用场景有所不同。以下是两者的主要区别: 1. 执行原理 防抖(Debounce): 执行条件&a…...

前端 Code Review 常见问题
在前端开发中,代码审查(Code Review)是一个至关重要的步骤。它不仅可以帮助团队成员之间共享知识和经验,还可以提高代码质量,减少错误和安全漏洞。以下是一些常见的前端 Code Review 问题和相应的解决方案。 1. 不一致…...

Python监控AWS ECS集群和服务的CPU和内存利用率
在电子商务或其他行业,重要节日通常会带来大量的流量和订单,这对应用程序的资源利用率提出了更高的要求。为了确保应用程序在节日期间能够顺利运行,提前监控和优化资源利用率至关重要。 在本文中,我们将介绍如何使用Python编写一个脚本,从AWS CloudWatch中获取ECS集群和服务的…...

淘宝天猫API接口深度解析:如何高效利用商品详情与关键词搜索商品列表功能
在电子商务的浩瀚海洋中,淘宝和天猫作为两大巨头,其平台上的商品信息无疑是商家和消费者关注的焦点。为了更高效地获取这些信息,淘宝天猫开放平台提供了丰富的API接口,其中商品详情接口和关键词搜索商品列表接口尤为关键。本文将深…...

python快速接入阿里云百炼大模型
1.注册阿里云账号 访问阿里云官网,完成账号注册流程,并开通百炼服务,网址:https://bailian.console.aliyun.com 2.获取 API Key 登录阿里云百炼平台,在个人中心或相关设置页面找到并生成 API Key,妥善保管此…...

基于AI对话生成剧情AVG游戏
游戏开发这个领域,一直有较高的学习门槛。作为一个非专业的游戏爱好者,如果想要开发游戏,往往受制于游戏引擎的专业程度,难以完成复杂的游戏项目。 AI IDE的诞生,提供了另外的一种思路,即通过AI 生成项目及…...

[flutter] 安卓编译配置
Maven 镜像 android/build.gradle buildscript {ext.kotlin_version 1.7.10repositories {google() // mavenCentral()maven { url https://maven.aliyun.com/repository/google }maven { url https://maven.aliyun.com/repository/jcenter }maven { url https://mav…...

使用ENSP实现NAT(2)
一、NAT的类型 二、静态NAT 1.项目拓扑 2.项目实现 路由器AR1配置: 进入系统视图 sys将路由器命名为AR1 sysname AR1关闭信息中心 undo info-center enable 进入g0/0/0接口 int g0/0/0将g0/0/0接口IP地址配置为192.168.10.254/24 ip address 192.168.10.254 24进…...

解决小程序中ios可以正常滚动,而Android失效问题
解决小程序中 iOS 可以正常滚动,而 Android 失效问题 在开发小程序时,我们经常会遇到一些平台兼容性问题。最近,我在开发一个小程序时遇到了一个问题:在 iOS 设备上可以正常滚动加载更多数据,而在 Android 设备上却无…...

docker安装部署
1.Docker简介 Docker是一个开源的容器引擎,开发者可以打包应用以及相关依赖包到一个可移植的容器中,发布到任何流行的Linux机器上。容器是完全使用沙箱机制,相互之间不会有任何接口,而且更轻量级。 1.1 概念 docker会自动搜索并下载应用镜像,镜像不仅包含应用本身,还包含…...