Android 事件分发机制详解/ 及Activity启动流程浅谈

news2025/1/11 0:44:32

目前网上的事件分发机制文章很多,可能大家看了很多,还是很懵逼,这篇文章是我自己总结的事件分发机制,提取了在事件分发机制中,最精华的部分,如果对事件分发还是不太清楚,可以看看这篇文章

更多精华可以在 vx 安卓新视界 免费获取 ,以上是都是免费文章
在这里插入图片描述
更多其他文章:
Android——四大组件及动画

一、事件分发机制

用户在使用app时,会经常频繁的触摸屏幕,点击页面交互,打开需要的功能。用户触摸点击的动作,就涉及到app中相关UI的触摸和点击事件。事件在UI中传递和处理的过程,就是事件分发流程。
发生一次点击事件时,事件会按照Activity->ViewGroup->View的顺序,进行事件传递。

  • Activity:控制UI页面的生命周期,是事件分发的入口。
  • ViewGroup:View的特殊子类,是一组View的集合,是Android中所有布局的父类。
  • View:所有UI组件的基类,常见的Button、TextView等控件都继承自View。

1.1 MotionEvent

[图片]

事件列,即指从手指接触屏幕至手指离开屏幕这个过程产生的一系列事件。一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件。
[图片]

1.2 事件分发过程由哪些方法协作完成?

在整个事件分发,并响应事件的过程中,有三个重要的方法:

  • dispatchTouchEvent:分发(传递)点击事件,当点击事件能够传递给当前View时,该方法就会被调用。
  • onInterceptTouchEvent:判断是否拦截某个事件,该方法仅在ViewGroup中存在。一般情况下会在ViewGroup的dispatchTouchEvent方法中调用该方法。
  • onTouchEvent:处理点击事件,在dispatchTouchEvent内部调用。
    [图片]

Activity的事件分发流程

Activity 中包含两个事件分发与处理的方法,分别是:

  • boolean dispatchTouchEvent(MotionEvent ev):事件分发
  • boolean onTouchEvent(MotionEvent event):事件消费
    当一个事件发生时,首先会将点击事件传递到Activity中,执行dispatchTouchEvent进行事件分发。经过window、decorView依次传递后,页面上的 ViewGroup会接收到该事件。ViewGroup如果消费了该事件,则分发结束,未消费则继续调用Activity的onTouchEvent 方法处理事件,简略流程图如下:
    [图片]

源码分析

源码分析:Activity.dispatchTouchEvent()
  public boolean dispatchTouchEvent(MotionEvent ev) {

    // 仅贴出核心代码

    // ->>分析1
    if (getWindow().superDispatchTouchEvent(ev)) {

        return true;
        // 若getWindow().superDispatchTouchEvent(ev)的返回true
        // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
        // 否则:继续往下调用Activity.onTouchEvent

    }
    // ->>分析3
    return onTouchEvent(ev);
  }

/**
  * 分析1:getWindow().superDispatchTouchEvent(ev)
  * 说明:
  *     a. getWindow() = 获取Window类的对象
  *     b. Window类是抽象类,其唯一实现类 = PhoneWindow类
  *     c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现
  */
  @Override
  public boolean superDispatchTouchEvent(MotionEvent event) {

      return mDecor.superDispatchTouchEvent(event);
      // mDecor = 顶层View(DecorView)的实例对象
      // ->> 分析2
  }

/**
  * 分析2:mDecor.superDispatchTouchEvent(event)
  * 定义:属于顶层View(DecorView)
  * 说明:
  *     a. DecorView类是PhoneWindow类的一个内部类
  *     b. DecorView继承自FrameLayout,是所有界面的父类
  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
  */
  public boolean superDispatchTouchEvent(MotionEvent event) {

      return super.dispatchTouchEvent(event);
      // 调用父类的方法 = ViewGroup的dispatchTouchEvent()
      // 即将事件传递到ViewGroup去处理,详细请看后续章节分析的ViewGroup的事件分发机制

  }
  // 回到最初的分析2入口处

/**
  * 分析3:Activity.onTouchEvent()
  * 调用场景:当一个点击事件未被Activity下任何一个View接收/处理时,就会调用该方法
  */
  public boolean onTouchEvent(MotionEvent event) {

        // ->> 分析5
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        
        return false;
        // 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕
    }

/**
  * 分析4:mWindow.shouldCloseOnTouch(this, event)
  * 作用:主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
  */
  public boolean shouldCloseOnTouch(Context context, MotionEvent event) {

  if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
          && isOutOfBounds(context, event) && peekDecorView() != null) {

        // 返回true:说明事件在边界外,即 消费事件
        return true;
    }

    // 返回false:在边界内,即未消费(默认)
    return false;
  } 

在这里插入图片描述

ViewGroup的事件分发流程

ViewGroup 中包含三个事件分发与处理的方法,分别是:

  • dispatchTouchEvent(MotionEvent ev):事件分发
  • onIntercepTouchEvent(MotionEvent ev):事件拦截
  • onTouchEvent(MotionEvent ev):事件消费 ViewGroup的事件分发机制从dispatchTouchEvent开始,接着调用onInterceptTouchEvent方法判断是否需要拦截事件。
    如果需要拦截,则表示当前 ViewGroup 希望处理该事件,或者不希望子 View 处理该事件,此时将直接调用 onTouchEvent方法处理事件。onTouchEvent方法如果消费了该事件,则分发结束,未消费则调用Activity的onTouchEvent方法处理事件。
    如果不需要拦截,则会遍历寻找被点击的子View,将该事件传递给子 View 的 dispatchTouchEvent 方法。如果未找到子View,事件将会继续传递给ViewGroup的onTouchEvent方法,与事件被拦截的效果一致。
    主要的事件分发链路图如下所示:
    [图片]

源码分析

 * 源码分析:ViewGroup.dispatchTouchEvent()
  */ 
  public boolean dispatchTouchEvent(MotionEvent ev) { 

  // 仅贴出关键代码
  ... 

  if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
  // 分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
    // 判断值1-disallowIntercept:是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
    // 判断值2-!onInterceptTouchEvent(ev) :对onInterceptTouchEvent()返回值取反
        // a. 若在onInterceptTouchEvent()中返回false,即不拦截事件,从而进入到条件判断的内部
        // b. 若在onInterceptTouchEvent()中返回true,即拦截事件,从而跳出了该条件判断
        // c. 关于onInterceptTouchEvent() ->>分析1

  // 分析2
    // 1. 通过for循环,遍历当前ViewGroup下的所有子View
    for (int i = count - 1; i >= 0; i--) {  
        final View child = children[i];  
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                || child.getAnimation() != null) {  
            child.getHitRect(frame);  

            // 2. 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
            if (frame.contains(scrolledXInt, scrolledYInt)) {  
                final float xc = scrolledXFloat - child.mLeft;  
                final float yc = scrolledYFloat - child.mTop;  
                ev.setLocation(xc, yc);  
                child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  

                // 3. 条件判断的内部调用了该View的dispatchTouchEvent()
                // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面章节介绍的View事件分发机制)
                if (child.dispatchTouchEvent(ev))  { 

                // 调用子View的dispatchTouchEvent后是有返回值的
                // 若该控件可点击,那么点击时dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                // 即该子View把ViewGroup的点击事件消费掉了

                mMotionTarget = child;  
                return true; 
                      }  
                  }  
              }  
          }  
      }  
    }  

  ...

  return super.dispatchTouchEvent(ev);
  // 若无任何View接收事件(如点击空白处)/ViewGroup本身拦截了事件(复写了onInterceptTouchEvent()返回true)
  // 会调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
  // 因此会执行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick(),即自己处理该事件,事件不会往下传递
  // 具体请参考View事件分发机制中的View.dispatchTouchEvent()

  ... 

}

/**
  * 分析1:ViewGroup.onInterceptTouchEvent()
  * 作用:是否拦截事件
  * 说明:
  *     a. 返回false:不拦截(默认)
  *     b. 返回true:拦截,即事件停止往下传递(需手动复写onInterceptTouchEvent()其返回true)
  */
  public boolean onInterceptTouchEvent(MotionEvent ev) {  
    
    // 默认不拦截
    return false;

  } 

Android事件分发传递到Acitivity后,总是先传递到ViewGroup、再传递到View。流程总结如下:(假设已经经过了Acitivity事件分发传递并传递到ViewGroup)
[图片]

View的事件分发流程

View 中包含如下两个事件分发与处理的方法,分别是:

  • dispatchTouchEvent(MotionEvent event)
  • onTouchEvent(MotionEvent event) View通过dispatchTouchEvent方法接收到从ViewGroup传递过来的事件后,直接调用 onTouchEvent方法处理事件。如果消费了该事件,则分发结束,未消费则调用ViewGroup的onTouchEvent方法处理事件。所以 View 中的事件处理流程很简单,如下图:
    [图片]
    此处说明一个细节,当View把事件消费后,如果View的onTouch方法返回true,View的dispatchTouchEvent方法会直接返回true,不会再调用View的onClick方法。只有当onTouch方法返回false时,才会有onClick事件处理。
    场景:拦截DOWN的后续事件
    结论
  • 若 ViewGroup 拦截了一个半路的事件(如MOVE),该事件将会被系统变成一个CANCEL事件 & 传递给之前处理该事件的子View;
  • 该事件不会再传递给ViewGroup 的onTouchEvent()
  • 只有再到来的事件才会传递到ViewGroup的onTouchEvent()
    场景描述
    ViewGroup B 无拦截DOWN事件(还是View C来处理DOWN事件),但它拦截了接下来的MOVE事件

即 DOWN事件传递到View C的onTouchEvent(),返回了true

实例

  • 在后续到来的MOVE事件,ViewGroup B 的onInterceptTouchEvent()返回true拦截该MOVE事件,但该事件并没有传递给ViewGroup B ;这个MOVE事件将会被系统变成一个CANCEL事件传递给View C的onTouchEvent()
  • 后续又来了一个MOVE事件,该MOVE事件才会直接传递给ViewGroup B 的onTouchEvent()
    后续事件将直接传递给ViewGroup B 的onTouchEvent()处理,而不会再传递给ViewGroup B 的onInterceptTouchEvent(),因该方法一旦返回一次true,就再也不会被调用了。
  • View C再也不会收到该事件列产生的后续事件

1.3 特殊说明

  • 若给控件注册了Touch事件,每次点击都会触发一系列action事件(ACTION_DOWN,ACTION_MOVE,ACTION_UP等)
  • 当dispatchTouchEvent()事件分发时,只有前一个事件(如ACTION_DOWN)返回true,才会收到后一个事件(ACTION_MOVE和ACTION_UP)
    即如果在执行ACTION_DOWN时返回false,后面一系列的ACTION_MOVE、ACTION_UP事件都不会执行

二、Activity解析

2.1 主要的几种Activity

暂时无法在Transsioner文档外展示此内容

在 Android 应用程序开发中,Activity 是一个关键组件,用于处理用户界面和用户交互。当开发 Android
应用程序时,通常会使用 Activity 类作为应用程序中每个屏幕的基础。 ComponentActivity 是一个继承自
FrgmentActivity 的类,它提供了与 AndroidX 组件兼容的 Activity 基础功能,比如支持
Fragment,支持生命周期,支持在旋转设备时保存 Activity 状态等等。 AppCompatActivity 是一个
Android 支持库中的类,用于在旧版 Android 平台上实现 Material Design 风格的应用程序。它扩展自
FragmentActivity,是 Activity 的子类,为 Android 应用程序提供了兼容性支持,使得应用程序可以在不同版本的
Android 平台上运行。 因此,ComponentActivity 和 AppCompatActivity 都是 Activity
的子类,但 ComponentActivity 是 AndroidX 组件兼容的 Activity 基础类,而
AppCompatActivity 则是提供兼容性支持的 Activity 类。 一般来说,如果应用程序需要支持 Material
Design,并且需要在旧版 Android 平台上运行,那么就应该使用 AppCompatActivity。而如果你的应用程序已经使用了
AndroidX 组件,并且不需要在旧版 Android 平台上运行,那么就应该使用 ComponentActivity。

2.2 Activity启动流程

我们手机的桌面是一个叫做Launcher的Activity,它罗列了手机中的应用图标,图标中包含安装apk时解析的应用默认启动页等信息。在点击应用图标时,即将要启动的App和Launcher、AMS、Zygote所属进程不同所以涉及到Launcher与AMS,AMS与Zygote,AMS与新App这四者多次通信,才会启动一个App,然后再启动Activity
[图片]

Activity启动 的 整体交互图:
[图片]

Launcher向AMS发送启动Activity
在用户点击应用图标时,Launcher这个Activity会调用startActivitySafely方法,最后调用到Activity.startActivity方法。

Launcher.java
   public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
       ...
       //标记在新的栈启动
       intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
       ...
       startActivity(intent, optsBundle);
       ...
   }

Activity.java
   @Override
   public void startActivity(Intent intent, @Nullable Bundle options) {
       ...
       if (options != null) {
           //-1为requestCode表明不需要知道是否启动成功
           startActivityForResult(intent, -1, options);
       } else {
           startActivityForResult(intent, -1);
       }
   }

   public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
           @Nullable Bundle options) {
           ...
           Instrumentation.ActivityResult ar =
               mInstrumentation.execStartActivity(
                   this, mMainThread.getApplicationThread(), mToken,this,intent, requestCode, options);
           ...
    }

里面调用了mInstrumentation.execStartActivity方法,其中一个参数mMainThread.getApplicationThread(),它的类型是ApplicationThread,ApplicationThread是ActivityThread的内部类,继承IApplicationThread.Stub,也是个Binder对象,在Activity工作流程中有重要作用。而Instrumentation具有跟踪application及activity生命周期的功能

Instrumentation.java
public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
    ...
        int result = ActivityTaskManager.getService().startActivity(whoThread,who.getBasePackageName(),
        who.getAttributionTag(),intent,intent.resolveTypeIfNeeded(who.getContentResolver()),
        token,target != null ? target.mEmbeddedID : null, requestCode, 0, null, options); 
    ...
}

ActivityTaskManager.java
public static IActivityTaskManager getService() {
       return IActivityTaskManagerSingleton.get();
}
    
private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
            new Singleton<IActivityTaskManager>() {
                @Override
                protected IActivityTaskManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
                    return IActivityTaskManager.Stub.asInterface(b);
                }
             }
};

这一步Launcher开始向AMS通信,由于在不同的进程所以需要通过Binder来通信,IActivityTaskManager是一个代理AMS端Binder的对象,之后AMS开始startActivity。
ATMS是在Android10中新增的,分担了之前ActivityManagerService(AMS)的一部分功能(activity task相关)。 在Android10 之前 ,这个地方获取的是服务是AMS。查看Android10的AMS,你会发现startActivity方法内也是调用了ATMS的startActivity方法。所以在理解上,ATMS就隶属于AMS。
PS:Activity的启动过程详解(基于Android10.0)

整体类图
[图片]

关联知识点:
1、Activity的显示原理(Window、DecorView、ViewRoot)
2、Activity的UI刷新机制(Vsync、Choreographer)
3、UI的绘制原理(Measure、Layout、Draw)
4、Surface原理(Surface、SurfaceFlinger服务):

Activity相关重要类含义

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2257050.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝桥杯刷题日记02-小球反弹

问题描述 有一长方形&#xff0c;长为 343720343720 单位长度&#xff0c;宽为 233333233333 单位长度。在其内部左上角顶点有一小球 (无视其体积)&#xff0c;其初速度如图所示且保持运动速率不变&#xff0c;分解到长宽两个方向上的速率之比为 dx:dy15:17dx:dy15:17。小球碰…

SkyWalking 和 ELK 链路追踪实战

一、背景 最近在给项目搭建日志平台的时候&#xff0c;采用的方案是 SkyWalking ELK 日志平台&#xff0c;但发现 ELK 日志平台中的日志没有 Trace ID&#xff0c;导致无法追踪代码报错的整体链路。 空哥提示&#xff1a;Trace ID 是分布式追踪中用来唯一标识一个服务请求或事…

Ubuntu22.04深度学习环境安装【cuda+cudnn】

为了复现一篇深度学习论文&#xff0c;特意安装了Linux系统。前一天已经安装Linux显卡驱动&#xff0c;现在需要安装cuda、cudnn等。 论文代码 论文PDF 确定包版本&#xff1a; 根据论文提供的代码。在requirements.txt中发现cuda版本为11.7,cudnn为8.5.0&#xff0c;python没…

ue5 motion matching

ue5.5 gameanimationsample 先看动画蓝图 核心两个node 第一个是根据数据选择当前的pose 第二个是缓存一段历史记录&#xff0c;为第一个node选择的时候提供数据。 在animinstance的update方法中 每帧都更新这个函数&#xff0c;每帧更新trajectory的数据 看看第一个node的…

mycat2 分库分表

mycat2读写分离 mysql主从复制 mycat2 分库分表 实在 配置mycat2 读写分离 及主从复制的基础上完成的 以下所有操作 在mycat 登录后执行 一、配置分库分表数据源 name:数据名称 一般以以最后一个字母判断 r 是读 w是写 url&#xff1a; 真实数据源ip地址 理论上是每一个ip都…

python pandas数据处理,对比两个xlsx文档的两列数据做对比匹配,然后复制单元格值

1&#xff0c;文档数据 文档A 批次编号编号风险等级意见ps001code1低风险同意ps001code2中风险同意ps001code3低风险同意ps001code4低风险同意ps001code5低风险同意ps001code6低风险同意ps002code11低风险同意ps002code12中风险否决ps002code13低风险否决ps002code14低风险否决…

Pytorch 手写数字识别 深度学习基础分享

本篇是一次内部分享&#xff0c;给项目开发的同事分享什么是深度学习。用最简单的手写数字识别做例子&#xff0c;讲解了大概的原理。 手写数字识别 展示首先数字识别项目的使用。项目实现过程: 训练出模型准备html手写板flask 框架搭建简单后端 简单手写数字识别 深度学习必…

WPS EXCEL 使用 WPS宏编辑器 写32位十六进制数据转换为浮点小数的公式。

新建EXCLE文件 另存为xlsm格式的文件 先打开WPS的开发工具中的宏编辑器 宏编辑器编译环境 在工作区添加函数并编译&#xff0c;如果有错误会有弹窗提示&#xff0c;如果没有错误则不会弹 函数名字 ”HEXTOFLOAT“ 可以自己修改。 function HEXTOFLOAT(hex) { // 将十六…

沃丰科技智能客服在跨境电商独立站中的核心角色

随着全球化进程的加速和互联网技术的不断发展&#xff0c;跨境电商行业蓬勃兴起&#xff0c;为消费者提供了更广阔、更便捷的购物选择。在这样一个竞争激烈的市场环境中&#xff0c;优质的客户服务成为了企业脱颖而出的关键。沃丰科技智能客服凭借其先进的技术和人性化的设计理…

langgraph实现无观测推理 (Reasoning without Observation)

图例 1. 图状态 在 LangGraph 中&#xff0c;每个节点都会更新一个共享的图状态。当任何节点被调用时&#xff0c;状态就是该节点的输入。 下面&#xff0c;我们将定义一个状态字典&#xff0c;用以包含任务、计划、步骤和其他变量。 from typing import List from typing…

2024企业数据资产入表合规指引——解读

更多数据资产资讯关注公众&#xff1a;数字化转型home 本报告旨在为企业数据资产入表提供合规保障。随着数字经济的发展&#xff0c;数据资产已成为重要战略资源和新生产要素。财政部发布的《企业数据资源相关会计处理暂行规定》明确&#xff0c;自2024年1月1日起&#xff0c;数…

19,[极客大挑战 2019]PHP1

这个好玩 看到备份网站字眼&#xff0c;用dirsearch扫描 在kali里打开 找出一个www.zip文件 访问一下 解压后是这个页面 class.php <?php include flag.php; error_reporting(0); class Name{ private $username nonono; private $password yesyes; public …

计算机键盘简史 | 键盘按键功能和指法

注&#xff1a;本篇为 “计算机键盘简史 | 键盘按键功能和指法” 相关文章合辑。 英文部分机翻未校。 The Evolution of Keyboards: From Typewriters to Tech Marvels 键盘的演变&#xff1a;从打字机到技术奇迹 Introduction 介绍 The keyboard has journeyed from a humb…

《Clustering Propagation for Universal Medical Image Segmentation》CVPR2024

摘要 这篇论文介绍了S2VNet&#xff0c;这是一个用于医学图像分割的通用框架&#xff0c;它通过切片到体积的传播&#xff08;Slice-to-Volume propagation&#xff09;来统一自动&#xff08;AMIS&#xff09;和交互式&#xff08;IMIS&#xff09;医学图像分割任务。S2VNet利…

HarmonyOS(65) ArkUI FrameNode详解

Node 1、Node简介2、FrameNode2.1、创建和删除节点2.2、对FrameNode的增删改2.3、 FramNode的查询功能3、demo源码4、总结5、参考资料1、Node简介 在HarmonyOS(63) ArkUI 自定义占位组件NodeContainer介绍了自定义节点复用的原理(阅读本本篇博文之前,建议先读读这个),在No…

Elasticsearch使用(2):docker安装es、基础操作、mapping映射

1 安装es 1.1 拉取镜像 docker pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/elasticsearch:7.17.3 1.2 运行容器 运行elasticsearch容器&#xff0c;挂载的目录给更高的权限&#xff0c;否则可能会因为目录权限问题导致启动失败&#xff1a; docker r…

java实现SpringBoot项目分页查询和消费的方法

简介 why&#xff1a; 最近在项目中&#xff0c;有一个sql需要查询100多万的数据&#xff0c;且需要在代码中遍历处理。面临两个问题 一次性查询出太多数据&#xff0c;速度较慢当前服务器内存支持以上操作&#xff0c;但是随着数据量的增多&#xff0c;以后可能会出现内存溢出…

专为高性能汽车设计的Armv9架构的Neoverse V3AE CPU基础知识与软件编码特性解析

一、ARMv9以及V3AE处理器架构 Armv9架构的Arm Neoverse V系列处理器是专为高性能计算设计的产品线&#xff0c;其中V3AE&#xff08;Advanced Efficiency&#xff09;特别强调了性能与效率之间的平衡。以下是关于Armv9架构下Neoverse V3AE处理器结构和指令集的一些详细解读&am…

Python数据清洗之重复数据处理

大家好&#xff0c;在数据处理和分析的过程中&#xff0c;重复数据是一个常见的问题。重复的数据不仅会影响数据的准确性&#xff0c;还可能导致模型训练中的偏差。因此&#xff0c;检测并清理重复数据是数据清洗中的重要步骤。Python 的 Pandas 提供了强大的功能来检测、标记和…

【实战教程】使用YOLO和EasyOCR实现视频车牌检测与识别【附源码】

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…