C#中的BeginInvoke和EndInvoke:异步编程的双剑客
文章目录
- 引言
- 1. BeginInvoke和EndInvoke的基本概念
- 1.1 什么是BeginInvoke和EndInvoke
- 1.2 重要概念解释
- 2. 委托中的BeginInvoke和EndInvoke
- 2.1 BeginInvoke方法
- 2.2 EndInvoke方法
- 2.3 两者的关系
- 3. 使用方式与模式
- 3.1 等待模式
- 3.2 轮询模式
- 3.3 等待句柄模式
- 3.4 回调模式
- 4. 底层实现原理
- 4.1 委托的底层模型
- 4.2 BeginInvoke的工作原理
- 4.3 EndInvoke的工作原理
- 5. 与现代异步编程的比较
- 5.1 Task-based Asynchronous Pattern (TAP)
- 5.2 优缺点比较
- 5.3 使用建议
- 6. 最佳实践与注意事项
- 6.1 始终调用EndInvoke
- 6.2 异常处理
- 6.3 线程安全性考虑
- 6.4 避免线程资源耗尽
- 7. 总结与展望
- 学习资源
引言
在C#的多线程编程中,BeginInvoke和EndInvoke是两个非常重要的方法,它们为开发者提供了一种简单而强大的异步编程模型。这两个方法允许我们在不阻塞主线程的情况下执行耗时操作,从而提高应用程序的响应性和性能。本文将深入探讨BeginInvoke和EndInvoke的工作原理、使用方法以及它们在现代C#编程中的定位。
1. BeginInvoke和EndInvoke的基本概念
1.1 什么是BeginInvoke和EndInvoke
BeginInvoke和EndInvoke是.NET Framework中提供的一对用于实现异步调用的方法。它们属于异步编程模型(APM),是早期.NET Framework中处理异步操作的标准方式。
- BeginInvoke:启动异步操作并立即返回,不等待操作完成
- EndInvoke:获取异步操作的结果,如果操作尚未完成则阻塞直到完成
这两个方法主要存在于两类对象中:
- 委托(Delegate):用于异步执行委托方法
- Windows窗体控件(Control):用于安全地从工作线程更新UI元素
需要注意的是,这两种情况下的BeginInvoke和EndInvoke功能和用途是不同的。本文将主要关注委托中的BeginInvoke和EndInvoke。
1.2 重要概念解释
在深入了解BeginInvoke和EndInvoke之前,我们需要理解几个重要概念:
- 同步调用:调用方法时,调用线程会等待方法执行完成才继续执行
- 异步调用:调用方法后,调用线程立即继续执行,不等待方法执行完成
- 回调:异步操作完成后执行的方法
- IAsyncResult:表示异步操作的接口,包含异步操作的状态和结果
2. 委托中的BeginInvoke和EndInvoke
在C#中,委托是一种类型安全的函数指针,可以引用具有特定参数列表和返回类型的方法。每个委托类型都自动具有BeginInvoke和EndInvoke方法,这些方法由CLR自动生成。
2.1 BeginInvoke方法
BeginInvoke方法启动异步调用,它具有以下特点:
- 参数与委托方法相同,外加两个可选参数:AsyncCallback回调和object状态对象
- 立即返回,不等待操作完成
- 返回IAsyncResult对象,用于跟踪异步操作状态
// BeginInvoke的典型签名
public IAsyncResult BeginInvoke([委托参数列表], AsyncCallback callback, // 可选的回调函数object state // 可选的状态对象
);
2.2 EndInvoke方法
EndInvoke方法用于获取异步操作的结果:
- 参数包括委托方法的输出参数和IAsyncResult对象
- 如果异步操作未完成,会阻塞调用线程直到操作完成
- 返回委托方法的返回值
- 负责释放异步操作使用的资源
// EndInvoke的典型签名
public [返回值类型] EndInvoke([out参数列表], IAsyncResult result // BeginInvoke返回的IAsyncResult对象
);
2.3 两者的关系
BeginInvoke和EndInvoke构成了一个完整的异步调用模式:
- BeginInvoke负责启动异步操作
- EndInvoke负责获取结果和清理资源
- 两者之间通过IAsyncResult对象关联
无论使用哪种模式调用BeginInvoke,都必须确保调用EndInvoke,否则可能导致资源泄露。
3. 使用方式与模式
使用BeginInvoke和EndInvoke有四种常见模式:
3.1 等待模式
最简单的模式是先调用BeginInvoke,然后在需要结果时调用EndInvoke:
// 定义一个计算密集型的委托
public delegate int CalculateDelegate(int value);public static void WaitPattern()
{// 创建委托实例CalculateDelegate calculate = new CalculateDelegate(ExpensiveCalculation);Console.WriteLine("开始异步计算...");// 异步调用IAsyncResult result = calculate.BeginInvoke(10, null, null);// 主线程继续执行其他工作Console.WriteLine("主线程继续执行其他工作...");// 在需要结果时调用EndInvoke,如果计算未完成会阻塞int calculationResult = calculate.EndInvoke(result);Console.WriteLine($"计算结果: {calculationResult}");
}// 模拟耗时计算
public static int ExpensiveCalculation(int value)
{// 模拟耗时操作Console.WriteLine("开始执行耗时计算...");Thread.Sleep(3000);Console.WriteLine("计算完成");return value * value;
}
3.2 轮询模式
使用IAsyncResult.IsCompleted属性定期检查异步操作是否完成:
public static void PollPattern()
{CalculateDelegate calculate = new CalculateDelegate(ExpensiveCalculation);// 开始异步计算IAsyncResult result = calculate.BeginInvoke(10, null, null);// 轮询检查操作是否完成while (!result.IsCompleted){// 显示进度或执行其他工作Console.Write(".");Thread.Sleep(200);}// 操作完成,获取结果int calculationResult = calculate.EndInvoke(result);Console.WriteLine($"\n计算结果: {calculationResult}");
}
3.3 等待句柄模式
使用IAsyncResult.AsyncWaitHandle属性获取WaitHandle,然后调用WaitOne方法等待异步操作完成:
public static void WaitHandlePattern()
{CalculateDelegate calculate = new CalculateDelegate(ExpensiveCalculation);// 开始异步计算IAsyncResult result = calculate.BeginInvoke(10, null, null);// 获取等待句柄WaitHandle waitHandle = result.AsyncWaitHandle;// 等待操作完成,最多等待5秒if (waitHandle.WaitOne(5000, false)){// 操作在超时前完成int calculationResult = calculate.EndInvoke(result);Console.WriteLine($"计算结果: {calculationResult}");}else{// 操作超时Console.WriteLine("操作超时!");}// 记得关闭等待句柄waitHandle.Close();
}
3.4 回调模式
使用AsyncCallback委托在异步操作完成时接收通知:
public static void CallbackPattern()
{CalculateDelegate calculate = new CalculateDelegate(ExpensiveCalculation);// 开始异步计算,指定回调方法calculate.BeginInvoke(10, CalculationCompleted, calculate);Console.WriteLine("主线程继续执行,不等待计算完成...");// 防止主线程退出Console.ReadLine();
}// 回调方法
private static void CalculationCompleted(IAsyncResult ar)
{// 从状态对象中获取委托CalculateDelegate calculate = (CalculateDelegate)ar.AsyncState;// 获取计算结果int result = calculate.EndInvoke(ar);Console.WriteLine($"异步回调: 计算结果 = {result}");
}
4. 底层实现原理
BeginInvoke和EndInvoke的底层实现涉及到.NET运行时的多个组件:
4.1 委托的底层模型
在.NET中,委托实际上是一个特殊的类,它继承自System.MulticastDelegate,后者继承自System.Delegate。委托类包含几个重要的字段:
// 简化的委托内部结构
public abstract class Delegate
{// 调用目标对象internal object _target; // 如果是静态方法则为null// 目标方法的指针internal IntPtr _methodPtr;// 静态方法的指针internal IntPtr _methodPtrAux;
}public abstract class MulticastDelegate : Delegate
{// 多播委托的调用列表private object _invocationList;private int _invocationCount;
}
4.2 BeginInvoke的工作原理
当调用委托的BeginInvoke方法时,以下过程会发生:
- CLR创建一个表示异步操作的对象(AsyncResult)
- 从线程池中获取一个工作线程
- 在工作线程上执行委托方法
- 立即向调用者返回AsyncResult对象
BeginInvoke方法不会在调用线程上执行委托方法,而是将执行工作委托给线程池,这样调用线程可以继续执行其他任务。
4.3 EndInvoke的工作原理
当调用EndInvoke方法时:
- 如果异步操作未完成,调用线程会阻塞直到操作完成
- 获取异步操作的结果或异常
- 释放与异步操作相关的资源
EndInvoke的实现使用了WaitHandle确保调用线程在异步操作完成前保持阻塞状态。
5. 与现代异步编程的比较
BeginInvoke和EndInvoke是.NET早期的异步编程模型,随着.NET的发展,出现了更现代的异步编程方式:
5.1 Task-based Asynchronous Pattern (TAP)
在现代.NET中,更推荐使用Task和async/await模式:
// 使用Task
public static async Task ModernAsyncExample()
{Console.WriteLine("开始异步操作...");// 使用Task.Run启动异步操作Task<int> calculationTask = Task.Run(() => ExpensiveCalculation(10));// 执行其他工作Console.WriteLine("主线程继续执行其他工作...");// 异步等待结果int result = await calculationTask;Console.WriteLine($"计算结果: {result}");
}
5.2 优缺点比较
BeginInvoke/EndInvoke与Task/async/await的比较:
特性 | BeginInvoke/EndInvoke | Task/async/await |
---|---|---|
代码复杂度 | 较高 | 较低 |
可读性 | 一般 | 优秀 |
组合操作 | 困难 | 简单 |
错误处理 | 复杂 | 简单,与同步代码类似 |
取消支持 | 需手动实现 | 内置支持 |
状态机生成 | 否 | 是,编译器生成 |
.NET版本 | 所有版本 | .NET 4.5+ |
5.3 使用建议
- 新项目:优先使用Task/async/await
- 维护老项目:可以继续使用BeginInvoke/EndInvoke,或考虑重构
- 需要兼容较老版本.NET:可能需要使用BeginInvoke/EndInvoke
值得注意的是,在.NET Core和.NET 5+中,委托的BeginInvoke和EndInvoke方法已被标记为过时,但在Windows Forms应用程序中,Control.BeginInvoke和Control.Invoke仍然是跨线程操作UI的推荐方式。
6. 最佳实践与注意事项
6.1 始终调用EndInvoke
无论使用哪种模式,都必须调用EndInvoke以释放资源:
// 错误示例 - 资源泄漏
delegate.BeginInvoke(param1, param2, null, null);
// 没有调用EndInvoke,可能导致资源泄漏// 正确示例
IAsyncResult result = delegate.BeginInvoke(param1, param2, null, null);
delegate.EndInvoke(result); // 确保调用EndInvoke
6.2 异常处理
异步操作中的异常在EndInvoke调用时抛出:
public static void ExceptionHandlingExample()
{CalculateDelegate calculate = new CalculateDelegate(CalculationWithException);IAsyncResult result = calculate.BeginInvoke(0, null, null);try{// 如果异步操作抛出异常,EndInvoke会重新抛出int calculationResult = calculate.EndInvoke(result);Console.WriteLine($"计算结果: {calculationResult}");}catch (DivideByZeroException ex){Console.WriteLine($"捕获到异常: {ex.Message}");}
}public static int CalculationWithException(int value)
{// 故意抛出异常return 100 / value; // 当value为0时抛出DivideByZeroException
}
6.3 线程安全性考虑
在异步回调中访问UI元素时,需要确保在UI线程上执行:
// Windows Forms示例
private void AsyncOperationButton_Click(object sender, EventArgs e)
{CalculateDelegate calculate = new CalculateDelegate(ExpensiveCalculation);// 启动异步操作calculate.BeginInvoke(10, (ar) => {// 获取结果int result = calculate.EndInvoke(ar);// 安全地更新UIthis.Invoke(new Action(() => {resultLabel.Text = $"计算结果: {result}";}));}, null);
}
6.4 避免线程资源耗尽
在短时间内创建大量异步操作时要小心,因为每个操作都会消耗线程池资源:
// 潜在问题 - 可能导致线程池资源耗尽
for (int i = 0; i < 1000; i++)
{calculate.BeginInvoke(i, null, null); // 不推荐
}// 更好的方式 - 控制并发度
int maxConcurrency = Environment.ProcessorCount * 2;
SemaphoreSlim semaphore = new SemaphoreSlim(maxConcurrency);for (int i = 0; i < 1000; i++)
{semaphore.Wait(); // 获取信号量calculate.BeginInvoke(i, (ar) => {try{calculate.EndInvoke(ar);}finally{semaphore.Release(); // 释放信号量}}, null);
}
7. 总结与展望
BeginInvoke和EndInvoke是.NET早期提供的异步编程模型,为开发者提供了在不阻塞主线程的情况下执行耗时操作的能力。尽管在现代.NET开发中,Task和async/await已经成为更推荐的异步编程方式,但理解BeginInvoke和EndInvoke的工作原理和使用方法仍然对以下方面有帮助:
- 维护使用这种模式的遗留代码
- 深入理解.NET异步编程的演变历程
- 在某些特定场景下需要更细粒度控制异步操作
随着.NET的不断发展,异步编程模型也在不断完善。尽管BeginInvoke和EndInvoke在新代码中的应用越来越少,但它们作为.NET异步编程历史的重要组成部分,承载了许多宝贵的设计经验,这些经验已经融入到现代异步编程模型中。
学习资源
- Microsoft文档:异步编程模型
- Microsoft文档:使用委托异步调用方法
相关文章:

C#中的BeginInvoke和EndInvoke:异步编程的双剑客
文章目录 引言1. BeginInvoke和EndInvoke的基本概念1.1 什么是BeginInvoke和EndInvoke1.2 重要概念解释 2. 委托中的BeginInvoke和EndInvoke2.1 BeginInvoke方法2.2 EndInvoke方法2.3 两者的关系 3. 使用方式与模式3.1 等待模式3.2 轮询模式3.3 等待句柄模式3.4 回调模式 4. 底…...

告别延迟!modbus tcp转profine网关助力改造电厂改造升级
发电需求从未如此旺盛。无论您是为客户发电还是为自身运营发电,您都需要提高运营效率,并在资产老化、资源萎缩的情况下,紧跟不断变化的法规。如今,智能系统和技术能够帮助您实现运营转型,提高可视性并实现关键流程自动…...

《软件工程》第 5 章 - 需求分析模型的表示
目录 5.1需求分析与验证 5.1.1 顺序图 5.1.2 通信图 5.1.3 状态图 5.1.4 扩充机制 5.2 需求分析的过程模型 5.3 需求优先级分析 5.3.1 确定需求项优先级 5.3.2 排定用例分析的优先顺序 5.4 用例分析 5.4.1 精化领域概念模型 5.4.2 设置分析类 5.4.3 构思分析类之间…...
解释k8s种ConfigMap和Secret的作用,如何在Pod中挂载环境变
一、ConfigMap & Secret 核心定位 属于Kubernetes的配置管理特性,用于解耦应用与配置 1. ConfigMap 作用:存储非敏感配置数据 存储内容: 环境变量命令行参数配置文件(如JSON/XML/YAML)系统参数(如J…...

阿里云国际版香港轻量云服务器:CN2 GIA加持,征服海外网络的“速度与激情”!
阿里云国际版香港轻量云服务器:CN2 GIA加持,征服海外网络的“速度与激情”! 面对全球化业务拓展对网络连接的严苛要求,阿里云国际版香港轻量云服务器正成为出海企业和开发者的新宠。其核心优势在于搭载了CN2 GIA(Glob…...

Qt6无法识别OpenCV(Windows端开发)
这段时间在Windows 10上进行Qt6的开发。结果在build过程中,出现了如下错误: 但实际上,我明明安装了OpenCV4.10.0, 并且也在CMakeLists.txt中加入了相关内容。 但是,注意自己的编译输出: [1/5 1.4/sec] Automatic MOC and UIC for target R…...

二、网络安全常见编码及算法-(2)
该文章主要介绍古典密码和隐写常用的密码和编码,日常中很少见,主要用于ctf比赛和考试学习一、古典密码 1、古典密码概念概述 古典密码是密码学发展早期所使用的一系列加密技术,这些密码主要依靠手工操作或简单的机械装置来实现信息的加密和…...

Windows系统安装MySQL Connector 使用C++ VS2022连接MySQL
1. 官网及版本 1.1. 网址 官方文档 - 安装编译构建: https://dev.mysql.com/doc/connector-cpp/9.3/en/ 官方文档 - 使用案例: https://dev.mysql.com/doc/dev/connector-cpp/latest/ 下载地址: https://dev.mysql.com/downloads/connector/…...

D2000平台上Centos使用mmap函数遇到的陷阱
----------原创不易,欢迎点赞收藏。广交嵌入式开发的朋友,讨论技术和产品------------- 在飞腾D2000平台上,安装了麒麟linux系统,我写了个GPIO点灯的程序,在应用层利用mmap函数将内核空间映射到用户态,然后…...

Elasticsearch索引机制与Lucene段合并策略深度解析
引言 在现代分布式搜索引擎Elasticsearch中,文档的索引、更新和删除操作不仅是用户交互的核心入口,更是底层存储架构设计的关键挑战。本文围绕以下核心链路展开: 文档生命周期管理:从客户端请求路由到分片定位,从内存…...
BPE、WordPiece 与 Unigram:三种主流子词分词算法对比
BPE、WordPiece 与 Unigram:三种主流子词分词算法对比 在构建现代自然语言处理模型时,Tokenizer 是连接文本与模型之间的桥梁。而在 tokenizer 的设计中,BPE(Byte Pair Encoding)、WordPiece 和 Unigram 三种子词&…...
青少年编程与数学 02-020 C#程序设计基础 11课题、可视化编程
青少年编程与数学 02-020 C#程序设计基础 11课题、可视化编程 一、可视化编程1. 降低学习门槛2. 提高学习兴趣3. 便于学习和掌握4. 为后续学习打下基础5. 适合不同年龄段和背景的初学者6. 适合初学者的可视化编程工具 二、可视化编程适合初学者1. 降低学习门槛2. 提高学习兴趣3…...
AI时代新词-AI驱动的自动化(AI - Driven Automation)
一、什么是AI驱动的自动化? AI驱动的自动化(AI - Driven Automation)是指利用人工智能技术实现各种流程和任务的自动化。这种自动化不仅包括简单的重复性任务,还涵盖了复杂的决策和优化任务。AI驱动的自动化通过机器学习、深度学…...

整合Jdk17+Spring Boot3.2+Elasticsearch9.0+mybatis3.5.12的简单用法
Elasticsearch是一个基于Lucene的分布式搜索和分析引擎,广泛应用于全文搜索、日志分析等场景。结合Spring Boot可以快速构建强大的搜索应用。本文将介绍如何在Spring Boot项目中集成和使用Elasticsearch。 ES9.0.1目前支持的包只有 elasticsearch-rest-client/ …...
Starrocks 物化视图的实现以及在刷新期间能否读数据
背景 本司在用Starrocks做一些业务上的分析的时候,用到了物化视图,并且在高QPS的情况下,RT也没有很大的波动,所以在此研究一下Starrock的实现,以及在刷新的时候是不是原子性的 本文基于Starrocks 3.3.5 结论 Starro…...
前后端传输 Long 类型数据时(时间戳,雪花算法ID),精度丢失的根本原因
前后端传输 Long 类型数据时,精度丢失的根本原因是 JavaScript 的 Number 类型无法精确表示超过 53 位(64 位双精度浮点数)的整数,而 Java 的 Long 类型是 64 位整数。当后端返回的 Long 值超过 2^53-1(即 90071992547…...
探索容器技术:Docker与Kubernetes的实践指南
随着云计算和微服务架构的兴起,容器技术已经成为软件开发和部署的新标准。容器技术以其轻量级、可移植性和灵活性等特点,为应用程序的快速部署、扩展和管理提供了强大的支持。在众多容器技术中,Docker和Kubernetes无疑是最受欢迎的两种。本文…...

Ubuntu从0到1搭建监控平台:本地部署到公网访问实战教程Cpolar穿透与Docker部署全过程
文章目录 前言1.关于Ward2.Docker部署3.简单使用ward4.安装cpolar内网穿透5. 配置ward公网地址6. 配置固定公网地址总结 前言 IT运维人员是否常为服务器管理系统的复杂操作所困扰?当海量性能指标图表与密集预警信号同时涌现时,这种信息过载往往让专业团…...

vscode java debug terminal 中文乱码
现象 解决 快捷键 ctrl , 进入setting 配文件添加 "terminal.integrated.automationProfile.windows": {"path": "cmd","args": ["/k","chcp","65001"]}terminal 启动时,活动也改为 utf-…...

3D PDF如何制作?SOLIDWORKS MBD模板定制技巧
SOLIDWORKS制作3D PDF模版 SOLIDWORKS MBD能够帮助工程师以清晰直观的方式描述产品尺寸信息。在3D PDF文件中,用户可以自由旋转和移动视图,方便查看模型的各个尺寸细节。 本文将带您一步步学习如何使用SOLIDWORKS MBD制作专业的3D PDF模板,…...

Qt DateTimeEdit(时间⽇期的微调框)
使⽤ QDateEdit 作为⽇期的微调框. 使⽤ QTimeEdit 作为时间的微调框 使⽤ QDateTimeEdit 作为时间⽇期的微调框. 这⼏个控件⽤法⾮常相似, 我们以 QDateTimeEdit 为例进⾏介绍. QDateTimeEdit 核⼼属性 属性说明dateTime时间⽇期的值. 形如 2000/1/1 0:00:00date单纯⽇期…...

C# 类和继承(屏蔽基类的成员)
屏蔽基类的成员 虽然派生类不能删除它继承的任何成员,但可以用与基类成员名称相同的成员来屏蔽(mask) 基类成员。这是继承的主要功能之一,非常实用。 例如,我们要继承包含某个特殊方法的基类。该方法虽然适合声明它的…...
基于qt5和stk10开发的互联调试
基于qt5和stk10开发的互联调试程序 QTSTK_Test_yuanwenjian/CppIncludes/AgAsHpopPlugin.tlb , 110080 QTSTK_Test_yuanwenjian/CppIncludes/agashpopplugin.tlh , 108623 QTSTK_Test_yuanwenjian/CppIncludes/AgAttrAutomation.tlb , 11408 QTSTK_Test_yuanwenjian/CppInclude…...
matlab雷达定位仿真
一、边扫描边跟踪雷达仿真 边扫描边跟踪(BISTAR)雷达仿真是一种实时雷达信号处理的技术,用于模拟雷达系统的操作过程,特别是那些具备连续扫描能力的雷达。它的基本原理和流程可以分为以下几个步骤: (1&…...

基于vue框架的动物园饲养管理系统a7s60(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
系统程序文件列表 项目功能:饲养员,健康登记,工作进度,动物信息,进食信息,动物健康,动物医治,饲料信息,工作留言 开题报告内容 基于Vue框架的动物园饲养管理系统开题报告 一、研究背景与意义 (一)研究背景 随着城市化进程加快和公众对生…...
MySQL 索引和事务
目录 前言 一、MySQL 索引介绍 1. 索引概述 2. 索引作用 3. 索引的分类 3.1 普通索引 3.2 唯一索引 3.3 主键索引 3.4 组合索引 (最左前缀) 3.5 全文索引 (FULLTEXT) 3.6 创建索引的原则依据 3.7 查看索引 3.8 删除索引 二、MySQL 事务 1. 事务的 ACID 原则 MYS…...
BERT分类器和朴素贝叶斯分类器比较
一、核心原理对比 维度预训练模型(如BERT)朴素贝叶斯分类器模型类型深度学习模型,基于Transformer架构,通过大规模无监督预训练学习语言表示。传统机器学习模型,基于贝叶斯定理和特征条件独立假设。特征表示自动学习文本的上下文相关表示(contextual embeddings),捕捉长…...

WPS自动换行
换行前 换行后 快捷键 第一步:启用「自动换行」功能 选中目标单元格/区域:点击需要设置的单元格(或拖动选中多个单元格)。开启自动换行(3种方式任选): 快捷按钮:在顶部菜单栏点击「…...
C#面向对象核心:类继承详解
类继承是什么? 继承是面向对象编程的三大特性之一,允许新类(派生类)基于已有类(基类)进行扩展。通过继承,派生类可以“免费”获得基类的字段、方法等成员,并添加自身独有的功能。 …...

maven中的grpc编译插件protobuf-maven-plugin详解
protobuf-maven-plugin 是 Maven 中用于编译 Protocol Buffers(protobuf)文件并生成对应语言代码(如 Java、C、Python 等)的插件。在 gRPC 项目中,它常被用来生成服务端和客户端所需的代码。以下是该插件的详细解析&am…...