Android事件分发机制
文章目录
- Android View事件分发机制:
- 事件分发中的核心方法
- onTouchListener和onClickListener的优先级
- 事件分发
- DOWN,MOVE,UP 事件分发
- CANCEL
- 代码实践
- requestdisallowIntereptTouchEvent作用
Android View事件分发机制:
事件分发中的核心方法
Android中事件分发,实际上分发的是MotionEvent,事件分发的过程中,涉及到下面三个核心的方法:
- dispatchTouchEvent:用来进行事件的分发,只要事件传递给当前View,那么这个方法一定会被调用,该方法的返回值受到onTouchEvent和子View的dispatchTouchEvent的影响。
- onInterceptTouchEvent:用来询问是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,这个方法不会被再次调用。(onInterceptTouchEvent只存在于ViewGroup中,Activity和普通的View中都没有这个方法)
- onTouchEvent:用来处理MotionEvent
上面的方法,如果返回的是true,就表示消耗当前的事件。
onTouchListener和onClickListener的优先级
onTouchListener 优先级高于 onClickListener
onTouchListener优先级高于onClickListener,onTouchListener返回false,后续的click事件才会被处理,onTouchListener返回true表示消耗了事件,不会再传递。
事件分发
事件传递的时候是由Activity->window->view,如果view不处理的话,最后事件会回到activity,在事件的流程中:View不会分发事件,View只会处理事件,ViewGroup会先分发事件,如果子View没有处理事件,尝试自己处理事件,如果自己没有处理,最后交给Activity。
DOWN,MOVE,UP 事件分发
android.view.ViewGroup#dispatchTouchEvent
boolean handled = false;if (onFilterTouchEventForSecurity(ev)) {final int action = ev.getAction();final int actionMasked = action & MotionEvent.ACTION_MASK;// Handle an initial down.if (actionMasked == MotionEvent.ACTION_DOWN) {// Throw away all previous state when starting a new touch gesture.// The framework may have dropped the up or cancel event for the previous gesture// due to an app switch, ANR, or some other state change.cancelAndClearTouchTargets(ev);// 一次事件开始,ViewGroup会先清除之前的状态,例如清楚:FLAG_DISALLOW_INTERCEPT的标记resetTouchState();}// Check for interception.final boolean intercepted;// 为down事件或者mFirstTouchTarget不为空表示找到了消耗touch事件的viewif (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {intercepted = onInterceptTouchEvent(ev);ev.setAction(action); // restore action in case it was changed} else {intercepted = false;}} else {// There are no touch targets and this action is not an initial down// so this view group continues to intercept touches.intercepted = true;}...// 会便利当前viewgroup的所有child,寻找是否需要消耗事件// Find a child that can receive the event.// Scan children from front to back.final ArrayList<View> preorderedList = buildTouchDispatchChildList();final boolean customOrder = preorderedList == null&& isChildrenDrawingOrderEnabled();final View[] children = mChildren;for (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);// If there is a view that has accessibility focus we want it// to get the event first and if not handled we will perform a// normal dispatch. We may do a double iteration but this is// safer given the timeframe.if (childWithAccessibilityFocus != null) {if (childWithAccessibilityFocus != child) {continue;}childWithAccessibilityFocus = null;i = childrenCount - 1;}// canReceivePointerEvents判断view是否可见&view没有再播放动画,// isTransformedTouchPointInView判断点击区域是否在view的范围内if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}
ViewGroup的TouchEvent中,都会先去判断View是否有子view,有子view的话递归调用子View的dispatchTouchEvent方法,否则直接调用自己的dispatchTouchEvent方法:
if (child == null) {handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}
Down事件分发后,一定会找到了消耗事件的view(或者事件直接被Activity消耗),如果ViewGroup没有消耗事件,事件就不会再继续往当前ViewGroup分发,UP和MOVE事件都会发送到DOWN事件的消耗这上,mFirstTouchTarget不为null,直接找到target进行分发。
// Dispatch to touch targets.if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {// Dispatch to touch targets, excluding the new touch target if we already// dispatched to it. Cancel touch targets if necessary.TouchTarget predecessor = null;TouchTarget target = mFirstTouchTarget;while (target != null) {final TouchTarget next = target.next;if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {handled = true;} else {final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}if (cancelChild) {if (predecessor == null) {mFirstTouchTarget = next;} else {predecessor.next = next;}target.recycle();target = next;continue;}}predecessor = target;target = next;}}
View的dispatchTouchEvent
android.view.View#dispatchTouchEvent
if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}//noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}if (!result && onTouchEvent(event)) {result = true;}}
View的dispatchTouchEvent中,会先判断view有没有设置TouchListener,如果设置了TouchListener再去判断onTouch方法的返回值,如果onTouch方法没有消耗事件,会再调用onTouchEvent方法。如果View 的onTouchEvent方法返回了false,就表示事件没有被消耗,那么最终会调用到ViewGroup的onTouchEvent。
// Dispatch to touch targets.if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);// 上面的代码会调用到dispatchTransformedTouchEvent中的下面的地方,进行事件分发:if (child == null) {handled = super.dispatchTouchEvent(transformedEvent);
mFirstTouchTarget为null表示子view没有消耗事件,这里会再调用dispatchTransformedTouchEvent去派发。
ViewGroup事件派发结束后,就会回到Activity中,参考下面的代码:
/*** Called to process touch screen events. You can override this to* intercept all touch screen events before they are dispatched to the* window. Be sure to call this implementation for touch screen events* that should be handled normally.** @param ev The touch screen event.** @return boolean Return true if this event was consumed.*/public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}if (getWindow().superDispatchTouchEvent(ev)) {return true;}// 执行activity的onTouchEventreturn onTouchEvent(ev);}
如果ViewGroup的onTouchEvent返回false,那么就会调用Activity的onTouchEvent。
CANCEL
Cancel事件触发场景案例:当ScrollView中添加自定义View时,ScrollView在DOWN事件不会进行拦截,当手指滑动到一定的距离后,onInterceptTouchEvent方法返回true,并触发ScrollView的滚动效果,当ScrollView进行滚动的时候,内部的子View会收到一个cancel事件,并丢失焦点。
代码实践
代码地址:https://gitee.com/lxd15130140362/lxd-android-start/tree/master/app/src/main/java/com/example/androidstart/view
界面布局样式:

- 都不处理事件,点击自定义的Textview日志顺序如下:
2023-02-19 20:50:19.786 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart D/CustomizeLayout: onInterceptTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart I/CustomizeTextView: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart I/CustomizeTextView: onTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.787 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_DOWN
2023-02-19 20:50:19.829 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:50:19.829 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:50:19.927 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:50:19.928 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:50:20.017 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:50:20.017 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:50:20.018 7129-7129/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_UP
2023-02-19 20:50:20.018 7129-7129/com.example.androidstart I/Activity: onTouchEvent: ACTION_UP
Down事件会从activity->viewgroup->view,move和up事件都被activity自己消费了,不会进行事件分发,因为之前的down事件没有人分发,就表示子view不会处理点击事件。
事件传输流程:

- 自定义的ViewGroup的onInterceptTouchEvent返回true,但是并不消耗事件,日志如下:
2023-02-19 20:57:04.896 7618-7618/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.898 7618-7618/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.898 7618-7618/com.example.androidstart D/CustomizeLayout: onInterceptTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.898 7618-7618/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.899 7618-7618/com.example.androidstart I/Activity: onTouchEvent: ACTION_DOWN
2023-02-19 20:57:04.995 7618-7618/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:57:04.996 7618-7618/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:57:04.998 7618-7618/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 20:57:04.998 7618-7618/com.example.androidstart I/Activity: onTouchEvent: ACTION_MOVE
2023-02-19 20:57:04.999 7618-7618/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_UP
2023-02-19 20:57:04.999 7618-7618/com.example.androidstart I/Activity: onTouchEvent: ACTION_UP
Down事件会从Activity->ViewGroup,因为ViewGroup进行了拦截,所以这里不会分发到子View,MOVE和UP事件也只会在activity中进行分发
- viewGroup的onInterceptTouchEvent调用父类实现,但是onTouchEvent返回truue,即ViewGroup不拦截但是消耗事件。
2023-02-19 21:17:28.939 9448-9448/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.940 9448-9448/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.941 9448-9448/com.example.androidstart D/CustomizeLayout: onInterceptTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.941 9448-9448/com.example.androidstart I/CustomizeTextView: dispatchTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.942 9448-9448/com.example.androidstart I/CustomizeTextView: onTouchEvent: ACTION_DOWN
2023-02-19 21:17:28.942 9448-9448/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_DOWN
2023-02-19 21:17:29.021 9448-9448/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_MOVE
2023-02-19 21:17:29.022 9448-9448/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_MOVE
2023-02-19 21:17:29.022 9448-9448/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_MOVE
2023-02-19 21:17:29.023 9448-9448/com.example.androidstart I/Activity: dispatchTouchEvent: ACTION_UP
2023-02-19 21:17:29.023 9448-9448/com.example.androidstart D/CustomizeLayout: dispatchTouchEvent: ACTION_UP
2023-02-19 21:17:29.023 9448-9448/com.example.androidstart I/CustomizeLayout: onTouchEvent: ACTION_UP
down事件会activity->viewgroup->view,因为viewgroup消耗了事件,因此down事件不会回到activity,同时由于已经有了事件消费者,因此MOVE和UP事件不会再往view进行传递,回直接调用到ViewGroup的onTouch中中。
requestdisallowIntereptTouchEvent作用
子view在自己的down或者move的时候调用requestdisallowIntereptTouchEvent,这样父view在这次事件传递中就不会拦截当前链路的事件。
链接:
requestDisallowInterceptTouchEvent失效的原因及解决
相关文章:
Android事件分发机制
文章目录Android View事件分发机制:事件分发中的核心方法onTouchListener和onClickListener的优先级事件分发DOWN,MOVE,UP 事件分发CANCEL代码实践requestdisallowIntereptTouchEvent作用Android View事件分发机制: 事件分发中的核心方法 Android中事件…...
python版协同过滤算法图书管理系统
基于协同过滤算法的图书管理系统 一、简介(v信:1257309054) 本系统基于推荐算法给用户实现精准推荐图书。 根据用户对物品或者信息的偏好,发现物品或者内容本身的相关性,或者是发现用户的相关性,然…...
Redis基础入门
文章目录前言一、redis是什么?二、安装步骤1.下载安装包2.安装三、Redis的数据类型redis是一种高级的key-value的存储系统,其中的key是字符串类型,尽可能满足如下几点:字符串(String)列表(List)集合(Set,不允许出现重复…...
【微服务】Feign实现远程调用和负载均衡
目录 1.什么是Feign 2 订单微服务集成Feign 2.1.引入依赖 2.2添加注解 2.3编写Feign的客户端 2.4修改OrderServiceImpl.java的远程调用方法 2.5重启订单服务,并验证 总结 1.什么是Feign Feign是Spring Cloud提供的⼀个声明式的伪Http客户端, 它…...
Windows使用QEMU搭建arm64 ubuntu 环境
1. 下载 QEMU: https://qemu.weilnetz.de/w64/ QEMU UEFI固件文件: https://releases.linaro.org/components/kernel/uefi-linaro/latest/release/qemu64/QEMU_EFI.fd arm64 Ubuntu镜像: http://cdimage.ubuntu.com/releases/20.04.3/rel…...
NodeJS安装
一、简介Node.js是一个让JavaScript运行在服务端的开发平台,Node.js不是一种独立的语言,简单的说 Node.js 就是运行在服务端的 JavaScript。npm其实是Node.js的包管理工具(package manager),类似与 maven。二、安装步骤…...
Gin 优雅打印请求与回包内容
文章目录1.Gin 的 Middleware2.使用 Middleware 打印请求与回包内容3.多次读取请求 Body 的问题4.多次读取响应 Body 的问题5.小结参考文献在开发 Web 应用程序时,难免不会遇到功能或性能等问题。为了快速定位问题,需要打印请求和响应的内容。本文将介绍…...
关于k8s中ETCD集群备份灾难恢复的一些笔记
写在前面 集群电源不稳定,或者节点动不动就 宕机,一定要做好备份,ETCD 的快照文件很容易受影响损坏。重置了很多次集群,才认识到备份的重要博文内容涉及 etcd 运维基础知识了解静态 Pod 方式 etcd 集群灾备与恢复 Demo定时备份的任务编写二进…...
【设计模式之美 设计原则与思想:设计原则】19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
关于 SOLID 原则,我们已经学过单一职责、开闭、里式替换、接口隔离这四个原则。今天,我们再来学习最后一个原则:依赖反转原则。在前面几节课中,我们讲到,单一职责原则和开闭原则的原理比较简单,但是&#x…...
2023年全国最新高校辅导员精选真题及答案13
百分百题库提供高校辅导员考试试题、辅导员考试预测题、高校辅导员考试真题、辅导员证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 一、单选题 131.下列不属于我国国土空间具有的特点的是() A.水资…...
【XXL-JOB】XXL-JOB定时处理视频转码
【XXL-JOB】XXL-JOB定时处理视频转码 文章目录【XXL-JOB】XXL-JOB定时处理视频转码1. 准备工作1.1 高级配置1.2 分片广播2. 需求分析2.1 作业分片方案2.2 保证任务不重复执行2.2.1 保证幂等性3. 视频处理业务流程3.1 添加待处理任务3.2 查询待处理任务3.3 更新任务状态3.4 工具…...
optuna用于pytorch的轻量级调参场景和grid search的自定义设计
文章目录0. 背景:why optuna0.1 插播一个简单的grid search0.2 参考1. Optuna1.1 a basic demo与部分参数释义1.2 random的问题1.3 Objective方法类2. Optuna与grid search4. optuna的剪枝prune5. optuna与可视化6. 未完待续0. 背景:why optuna 小模型参…...
语法篇--汇编语言先导浅尝
一、相关概念 1.机器语言 机器语言(Machine Language)是一种计算机程序语言,由二进制代码(0和1)组成,可被计算机直接执行。机器语言是计算机硬件能够理解和执行的唯一语言。 机器语言通常由一系列的指令组…...
【ID:17】【20分】A. DS顺序表--类实现
时间限制1秒内存限制128兆字节题目描述用C语言和类实现顺序表属性包括:数组、实际长度、最大长度(设定为1000)操作包括:创建、插入、删除、查找类定义参考输入第1行先输入n表示有n个数据,即n是实际长度;接着输入n个数据…...
【java web篇】Tomcat的基本使用
📋 个人简介 💖 作者简介:大家好,我是阿牛,全栈领域优质创作者。😜📝 个人主页:馆主阿牛🔥🎉 支持我:点赞👍收藏⭐️留言Ὅ…...
MySQL实战解析底层---行锁功过:怎么减少行锁对性能的影响
目录 前言 从两阶段锁说起 死锁和死锁检测 前言 MySQL 的行锁是在引擎层由各个引擎自己实现的但并不是所有的引擎都支持行锁,比如MyISAM 引擎就不支持行锁不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任何时刻只能有…...
初识STM32单片机
目录 初识STM32单片机 什么是单片机? STM系列单片机命名规则 STM32F103C8T6单片机简介 标准库与HAL库区别 通用输入输出端口GPIO 什么是GPIO? 定义 命名规则 内部框架图 推挽输出与开漏输出 如何点亮一颗LED灯 编程实现点灯 按键点亮LED灯…...
数据结构与算法系列之单链表
💗 💗 博客:小怡同学 💗 💗 个人简介:编程小萌新 💗 💗 如果博客对大家有用的话,请点赞关注再收藏 🌞 这里写目录标题test.hSList.h注意事项一级指针与二级指针的使用assert的使用空…...
MySQL基础
本单元目标 一、为什么要学习数据库 二、数据库的相关概念 DBMS、DB、SQL 三、数据库存储数据的特点 四、初始MySQL MySQL产品的介绍 MySQL产品的安装 ★ MySQL服务的启动和停止 ★ MySQL服务的登录和退出 ★ MySQL的常见命令和语法规范 五、…...
面试热点题:环形链表及环形链表寻找环入口结点问题
环形链表 问题: 给你一个链表的头节点 head ,判断链表中是否有环。 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
《Playwright:微软的自动化测试工具详解》
Playwright 简介:声明内容来自网络,将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具,支持 Chrome、Firefox、Safari 等主流浏览器,提供多语言 API(Python、JavaScript、Java、.NET)。它的特点包括&a…...
DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...
