当前位置: 首页 > news >正文

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!!}

线程池工作原理:

  1. 工作中线程 < 核心线程数 创建新线程
  2. 工作中线程 > 核心线程数且工作队列未满,加入工作队列
  3. 工作队列已满,工作中线程数若 < 最大线程数, 创建新线程
  4. 工作队列已满,工作中线程数 > 最大线程数, 执行拒绝策略(默认为抛出异常,可自定义)

在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.拦截器工作原理

拦截器的工作基本分为三步:

  1. 处理请求request
  2. 将请求传往下一拦截器,获取返回的请求response
  3. 处理响应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复用

目录 一&#xff0c;分发器和拦截器 二&#xff0c;分发器处理异步请求 1.分发器处理入口 2.分发器工作流程 3.分发器中的线程池设计 三&#xff0c;分发器处理同步请求 四&#xff0c;拦截器处理请求 1.责任链设计模式 2.拦截器工作原理 3.OkHttp五大拦截器 一&#…...

中国计算机学会计算机视觉专委会携手合合信息举办企业交流活动,为AI安全治理打开“新思路”

近期&#xff0c;《咬文嚼字》杂志发布了2024年度十大流行语&#xff0c;“智能向善”位列其中&#xff0c;过去一年时间里&#xff0c;深度伪造、AI诈骗等话题屡次登上热搜&#xff0c;AI技术“野蛮生长”引发公众担忧。今年9月&#xff0c;全国网络安全标准化技术委员会发布了…...

重生之我在异世界学编程之C语言:深入预处理篇(上)

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 本文目录 引言正文一、预处理的作用与流程&#xf…...

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

文章目录 知识回顾一、栈&#xff08;Stack&#xff09;和堆&#xff08;Heap&#xff09;1、什么是栈和堆2、为什么要分栈和堆3、栈和堆的区别栈堆 4、总结 二、值类型和引用类型1、那么值类型和引用类型到底有什么区别呢&#xff1f;值类型引用类型 2、总结 三、特殊的引用类…...

ARCGIS国土超级工具集1.2更新说明

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

暂停window11自动更新

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

Git简介和特点

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

如何通过docker 部署minio,端口号为9105

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

设置Qt程序开机自启动(windows版本)

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

【HarmonyOS】鸿蒙获取appIdentifier,Identifier

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

【Rust自学】3.5. 控制流:if else

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

美国信息学奥林匹克竞赛USACO 2024年12月比赛铜级问题1. 循环舍入-答案代码

题目见&#xff1a; USACO 2024年12月比赛铜级问题1. 循环舍入(USACO 2024 December Contest, Bronze Problem 1. Roundabount Rounding) 最简单的青铜级 亲测所有得分点通过哈&#xff01; 下一篇会给解题分析哦&#xff01; #include <iostream> using namespace std;…...

Llama3模型详解 - Meta最新开源大模型全面解析

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

2021-02-12 c++里面cin.sync()函数的意思

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

下载红米Note 9 Pro5G对应的LineageOS代码下载及编译

构建 LineageOS 进入网站&#xff1a;Info about gauguin | LineageOS Wiki&#xff0c;点击&#xff1a;Build for yourself&#xff0c;里面有详细的教程&#xff0c;我这里就按照Note 9 Pro 5G来。 机器环境 Ubuntu环境为&#xff1a;20.04.6LinagesOS版本&#xff1a;21-…...

《探索 Caffe2 的 C++接口在移动设备上的性能优化之路》

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

1.编写一个程序,给定一个大写字母,要求用小写输出

思路&#xff1a;ascII码值 例如&#xff1a;A的ASCII码值为65&#xff0c;a的ASCII码值为97&#xff0c;不难发现&#xff0c;大小写字母之间的ascii值相差了32 #include <stdio.h> int main() {char c;scanf("%c",&c);printf("%c",c32);retu…...

条件随机场(CRF)详解:原理、算法与实现(深入浅出)

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

Android Studio、JDK、AGP、Gradle、kotlin-gradle-plugin 兼容性问题

文章目录 问题&#xff1a;解决办法&#xff1a;gradle与 java的版本兼容AGP与Gradle的版本兼容kotlin 与 jvm 的版本兼容KGP、Gradle、AGP兼容关系kotlin 与 java 的编译版本配置 问题&#xff1a; 你从githb上clone了一个项目&#xff0c;本地跑的时候&#xff0c;各种报错。…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

day52 ResNet18 CBAM

在深度学习的旅程中&#xff0c;我们不断探索如何提升模型的性能。今天&#xff0c;我将分享我在 ResNet18 模型中插入 CBAM&#xff08;Convolutional Block Attention Module&#xff09;模块&#xff0c;并采用分阶段微调策略的实践过程。通过这个过程&#xff0c;我不仅提升…...

Debian系统简介

目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版&#xff…...

el-switch文字内置

el-switch文字内置 效果 vue <div style"color:#ffffff;font-size:14px;float:left;margin-bottom:5px;margin-right:5px;">自动加载</div> <el-switch v-model"value" active-color"#3E99FB" inactive-color"#DCDFE6"…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1

每日一言 生活的美好&#xff0c;总是藏在那些你咬牙坚持的日子里。 硬件&#xff1a;OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写&#xff0c;"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

CMake控制VS2022项目文件分组

我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...

laravel8+vue3.0+element-plus搭建方法

创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

Mobile ALOHA全身模仿学习

一、题目 Mobile ALOHA&#xff1a;通过低成本全身远程操作学习双手移动操作 传统模仿学习&#xff08;Imitation Learning&#xff09;缺点&#xff1a;聚焦与桌面操作&#xff0c;缺乏通用任务所需的移动性和灵活性 本论文优点&#xff1a;&#xff08;1&#xff09;在ALOHA…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

C# 表达式和运算符(求值顺序)

求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如&#xff0c;已知表达式3*52&#xff0c;依照子表达式的求值顺序&#xff0c;有两种可能的结果&#xff0c;如图9-3所示。 如果乘法先执行&#xff0c;结果是17。如果5…...