Skip to content

View事件分发机制

zhpanvip edited this page Feb 16, 2021 · 23 revisions

View事件分发

1.事件分发机制流程

Android系统中的输入事件为InputEvent,InputEvent又分为KeyEvent和MotionEvent。KeyEvent对应着键盘输入事件,而MotionEvent则对应着屏幕触摸事件。这两种事件都由InputManager统一分发。 在系统启动时,SystemServer会启动窗口管理服务WindowManagerService,在WindowManagerService中会起动输入管理器InputManager来负责监控键盘消息。 InputManager负责从硬件接受输入事件,并将事件分发给当前激活的窗口处理,而InputManager与Window之间的通信是通过ViewRootImpl类实现的。 ActivityThread负责启动Activity,在performaLaunchActivity流程中,ActivityThread会为Activity创建PhoneWindow和DocerView,然后在handleResumeActivity()中会将PhoneWindow和InputManagerService建立起链接,保证UI可见时能够对输入事件进行正确的分发。

当InputManager监控到硬件层的输入事件时,会通知ViewRootImpl对事件进行底层分发。

UI层级的事件分发 作为 完整事件分发流程的一部分,发生在ViewPostImeInputStage.processPointerEvent

final class ViewPostImeInputStage extends InputStage {

  private int processPointerEvent(QueuedInputEvent q) {
    // 让顶层的View开始事件分发
    final MotionEvent event = (MotionEvent)q.mEvent;
    boolean handled = mView.dispatchPointerEvent(event);
    //...
  }
}

这个顶层的View其实就是DecorView,DecorView实际上就是Activity中Window的根布局,它是一个FrameLayout。 DocerView在接受到事件后首先分发给了Activity,Activity又将事件分发给了Window,最终Window又将事件交回给DocerView,在接下来就是UI层的事件分发了。

(1)事件分发的流程 对于VieGroup的事件分发来说其本质就是递归,将本身视为上游的ViewGroup需要自定义dispatchTouchEvent(),并调用child的dispatchTouchEvent()将事件分发给下游的子View。同时,在归流程中,本身视为一个View,需要调用View自身的方法(super.dispatchTouchEvent())来决定是否消费该事件,并将结果返回上游,直至回归到View树的根节点。

对于事件分发整体流程,我们可以进行如下定义:

  • 1.ViewGroup将事件分发给子View,当子View接受到事件后,若其有child,则通过dispatchTouchEvent()继续讲事件分发给child。
  • 2.子View接受到事件后,通过自身的dispatchTouchEvent方法来判断事件是否被消费掉了。 2.1 若该View消费事件,则响上层ViewGroup返回true,上层ViewGroup接收到true后则认为事件已被消费,继续向其上册ViewGroup返回true。 2.2 若该View为消费事件,则向上层ViewGroup返回false,上层ViewGroup接收到false后调用自身的dispatchTouchEvent来决定是否消费事件,并最终将是否消费返回到上层View。 其伪代码如下:
// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
  boolean consume = false;
  // 1.将事件分发给Child
  if (hasChild) {
    consume = child.dispatchTouchEvent();
  }
  // 2.若Child不消费该事件,或者没有child,判断自身是否消费该事件
  if (!consume) {
    consume = super.dispatchTouchEvent();
  }
  // 3.将结果向上层传递
  return consume;
}

构建事件序列分发链 在事件分发过程中,如果都对View进行一次递归遍历,是一个很好性能的过程,因此对其进行了优化。当接收到ACTIVITY_DOWN事件时,以为着一个完整序列的开始。通过递归遍历找到View中真正对事件进行消费的View,并将其保存。折后接收到ACTIVITY_MOVE和ACTIVITY_UP事件时则跳过递归过程,将事件直接分发给Child。

在ViewGroup中有一个mFirstTouchTarget的成员变量,用来保存消费事件的View,代码如下:

public abstract class ViewGroup extends View {
    // 指向下一级事件分发的`View`
    private TouchTarget mFirstTouchTarget;

    private static final class TouchTarget {
        public View child;
        public TouchTarget next;
    }
}

每个ViewGroup都持有一个mFirstTouchTarget, 当接收到一个ACTION_DOWN时,通过递归遍历找到View树中真正对事件进行消费的Child,并保存在mFirstTouchTarget属性中,依此类推组成一个完整的分发链。 在这里插入图片描述 在这之后,当接收到同一事件序列的其它事件如ACTION_MOVE、ACTION_UP时,则会跳过递归流程,将事件直接分发给 分发链 下一级的Child:

// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
  boolean consume = false;
  // ...
  if (event.isActionDown()) {
    // 1.第一次接收到Down事件,递归寻找分发链的下一级,即消费该事件的View
    // 这里可以看到,递归深度搜索的算法只执行了一次
    mFirstTouchTarget = findConsumeChild(this);
  }

  // ...
  if (mFirstTouchTarget == null) {
    // 2.分发链下一级为空,说明没有子`View`消费该事件
    consume = super.dispatchTouchEvent(event);
  } else {
    // 3.mFirstTouchTarget不为空,必然有消费该事件的`View`,直接将事件分发给下一级
    consume = mFirstTouchTarget.child.dispatchTouchEvent(event);
  }
  // ...
  return consume;
}

事件拦截机制 ViewGroup中通过onInterceptTouchEvent来决定是否拦截该事件,

// 伪代码实现
// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
  // 1.若需要对事件进行拦截,直接中止事件向下分发,让自身决定是否消费事件,并将结果返回
  if (onInterceptTouchEvent(event)) {
    return super.dispatchInputEvent(event);
  }
  // ...
  // 2.若不拦截当前事件,开始事件分发流程
}

为了避免额外的开销,设计者根据 事件序列 为 事件拦截机制 做出了额外的优化处理,保证了 事件拦截的判断在一个事件序列中只处理一次,伪代码简单实现如下:

// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
  if (mFirstTouchTarget != null) {
    // 1.若需要对事件进行拦截,直接中止事件向下分发,让自身决定是否消费事件,并将结果返回
    if (onInterceptTouchEvent(event)) {
      // 2.确定对该事件序列拦截后,因此就没有了下一级要分发的Child
      mFirstTouchTarget = null;
      // 下一个事件传递过来时,最外层的if判断就会为false,不会再重复执行onInterceptTouchEvent()了
      return super.dispatchInputEvent(event);
    }
  }

  // ...
  // 3.若不拦截当前事件,开始事件分发流程
}

2.ViewGroup中的mFirstTouchTarget是一个什么东西,它有什么作用?

在ViewGroup中有一个类型为TouchTrarget的mFirstTouchTarget的成员变量,它是用来保存消费事件的子View的信息的。代码如下:

private static final class TouchTarget {
        @UnsupportedAppUsage
        public View child;
        public TouchTarget next;
       // ...省略无关代码
    }

可以看到TouchTarget内部保存了一个View和一个类型为TouchTarget的next成员变量,也就是说TouchTarget是一个链表结构。为什么是链表结构呢?主要是因为Android系统是支持多点触控的,所以TouchTarget设计成了链表。 设计mFirstTouchTarget的目的是为了避免在所有的事件序列中都去递归查找要消费事件的View,只需要在ACTION_DOWN中递归查找消费的View,并将View封装后赋值为mFirstTouchTarget,避免了后续一系列事件的查找。 mFirstTouchTarget会在ACTION_DOWN的时候被赋值,查找是否有能够消费事件的子View,如果有则将这个View包装成TouchTarget赋值给mFirstTouchTarget,否则mFirstTouchTarget为null。 接下来的一系列ACTION_MOVE事件都会根据mFirstTouchTarget是否为null和onInterceptTouchEvent来判断是否要拦截事件。所以mFirstTouchTarget在事件分发的流程中占了非常重要的作用。

3.如果在ViewGroup中拦截了ACTION_DOWN事件会怎样?

首先来看ViewGroup的dispatchTouchEvent方法的伪代码:

// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
             // 调用自身的onInterceptTouchEvent方法来判断是否拦截事件
            intercepted = onInterceptTouchEvent(ev);
        } else {
            intercepted = true;
        }
        // 如果在ACTION_DOWN中拦截了事件,那么intercepted恒为true,则无法给mFirstTouchTarget赋值
        if (!canceled && !intercepted) {
            mFirstTouchTarget = findConsumeChild(this);
        }
        if (mFirstTouchTarget == null) {  
        	// 则调用自身的dispatchTouchEvent方法分发事件             
            handled = super.dispatchTouchEvent(event);
        } else{
        	// 则调用子View的dispatchTouchEvent方法
			handled = mFirstTouchTarget.child.dispatchTouchEvent(event);
		}
        return handled;
    }

从上面的代码中可以看到,如果在onInterceptTouchEvent方法中拦截ACTION_DOWN事件,则intercepted为true,而intercepted为true直接导致了mFirstTouchTarget无法被赋值。

接下来,一系列ACTION_MOVE以及ACTION_UP等事件都无法再调用onInterceptTouchEvent方法,也就是intercepted恒为true,且mFirstTouchTarget恒为null。

再往下,由于mFirstTouchTarget恒为null,就导致了所有的Motion事件(也包括ACTION_DOWN事件)只能交由自身处理,无法再将事件分发给子View。

当然,在大部分情况下这样的代码可能并不会有问题,因为事件可能就是需要这个ViewGroup来拦截并且消费的。但是,如果此时子View需要拦截事件就无能为力了。例如,很多情况下需要通过内部拦截法来处理滑动冲突,而此时由于子View的dispatchTouchEvent方法根本就没有被调用,内部拦截法自然也无能为力。

4.为什么设置了onTouchListener后onClickListener不会被调用?

// View
public boolean dispatchTouchEvent(MotionEvent event) {
            //noinspection SimplifiableIfStatement
            ListenerInfo 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;
            }
        }

        return result;
    }

在View的dispatchTouchEvent中如果li.mOnTouchListener不为null,则调用li.mOnTouchListener.onTouch,而如果li.mOnTouchListener.onTouch返回了true,则下边的onTouchEvent就不会被调用,而onClickListener就是在onTouchEvent方法中才被调用的。

// 伪代码
public boolean onTouchEvent(MotionEvent event) {
	public boolean onTouchEvent(MotionEvent event) {
		case MotionEvent.ACTION_UP:
			li.mOnClickListener.onClick(this);
		break;
	}
}

5.为什么一个View设置了setOnTouchListener会有提示没有引用performClick方法的警告?

当你添加了一些点击操作,例如像setOnClickListener这样的,它会调用performClick才可以完成onClick方法的调用,但你重写了onTouch,就有可能使得performClick没有被调用,这样这些点击操作就没办法完成了,所以就会有了这个警告。

参考 反思|Android 事件分发机制的设计与实现 反思|Android 事件拦截机制的设计与实现 图解 Android 事件分发机制

屏幕刷新机制

屏幕刷新机制概述

在一个典型的显示系统中,一般包括CPU、GPU、display三个部分, CPU负责计算数据,把计算好数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer里存起来,然后display负责把buffer里的数据呈现到屏幕上。很多时候,我们可以把CPU、GPU放在一起说,那么就是包括2部分,CPU/GPU 和display。

tearing: 一个屏幕内的数据来自2个不同的帧,画面会出现撕裂感 jank: 一个帧在屏幕上连续出现2次 lag:从用户体验来说,就是点击下去到呈现效果之间存在延迟 屏幕刷新频率:一秒内屏幕刷新多少次,或者说一秒内显示了多少帧的图像,屏幕扫描是从左到右,从上到下执行的。显示器并不是一整个屏幕一起输出的,而是一个个像素点输出的,我们看不出来,是因为速度太快了,人有视觉暂留,所以看不出来。

为什么会产生tearing? 显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,display从buffer中取出数据,然后一行一行显示出来。display处理的频率是固定的,比如每隔60ms显示完一帧,但是CPU/GPU写数据是不可控的,所以会出现有些数据根本没显示出来就被重写了,buffer里的数据可能是来自不同的帧的, 所以出现画面“割裂”。 怎么解决tearing问题? 可以使用双缓存来解决tearing问题,基本原理就是采用两块buffer。一块back buffer用于CPU/GPU后台绘制,另一块framebuffer则用于显示,当back buffer准备就绪后,它们才进行交换。不可否认,double buffering可以在很大程度上降低screen tearing错误。 double buffering存在什么问题? 在这里插入图片描述 以时间的顺序来看下将会发生的异常: Step1. Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,而且赶在Display显示下一帧前完成 Step2. 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,正常显示第1帧 Step3. 由于某些原因,比如CPU资源被占用,系统没有及时地开始处理第2帧,直到第2个VSync快来前才开始处理 Step4. 第2个VSync来时,由于第2帧数据还没有准备就绪,显示的还是第1帧。这种情况被Android开发组命名为“Jank”。 Step5. 当第2帧数据准备完成后,它并不会马上被显示,而是要等待下一个VSync。 所以总的来说,就是屏幕平白无故地多显示了一次第1帧。原因大家应该都看到了,就是CPU没有及时地开始着手处理第2帧的渲染工作,以致“延误军机”。 Android在4.1之前一直存在这个问题。

Android系统是如何解决双缓存存在的问题的? 为了优化显示性能,android 4.1版本对Android Display系统进行了重构,实现了Project Butter,引入了三个核心元素,即VSYNC、Triple Buffer和Choreographer。

丢帧一般是什么原因引起的?

主线程有耗时操作,耽误了View的绘制。

Android刷新频率60帧/秒,每隔16ms调onDraw绘制一次?

显示器每隔16ms会刷新一次,但是只有用户发起重绘请求才会调用onDraw。

onDraw完之后屏幕会马上刷新么?

不会,会等待下一个Vsync信号。

如果界面没有重绘,还会每隔16ms刷新屏幕么?

对于底层显示器,每间隔16.6ms接收到VSYNC信号时,就会用buffer中数据进行一次显示。所以一定会刷新。(用的旧的数据)

如果在屏幕快刷新的时候才去onDraw绘制会丢帧么

代码发起的View的重绘不会马上执行,会等待下次VSYNC信号来的时候才开始。什么时候绘制没影响。

如果快速调用10次requestLayout,会调用10次onDraw吗?

mTraversalScheduled这个变量是为了过滤一帧内重复的刷新请求,初始值是false,在开始这一帧的绘制流程时候也会重新置为false(doTraversal()中,一会儿分析),同时,在取消遍历绘制 View 操作 unscheduleTraversals() 里也会设置为false。也就是说一般情况下在开始这一帧的正式绘制前,在这期间重复调用scheduleTraversals()只有一次会生效。这么设计的原因是前面已经说了和ViewRootImpl绑定的是DecorView,当刷新时候会对整个DecorView进行一次处理,所以不同view触发的scheduleTraversals()作用都是一样的,所以在这一帧里面只要有一次和多次刷新请求效果是一样的。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true; //防止多次调用
            // 发送同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
        }
    }

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            // 移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            ...
            performTraversals();
            ...
        }
    }

简述UI渲染流程

(1)界面上任何一个 View 的刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里来安排一次遍历绘制 View 树的任务。

(2)scheduleTraversals() 会先过滤掉同一帧内的重复调用,确保同一帧内只需要安排一次遍历绘制 View 树的任务,遍历过程中会将所有需要刷新的 View 进行重绘。

(3)scheduleTraversals() 会往主线程的消息队列中发送一个同步屏障,拦截这个时刻之后所有的同步消息的执行,但不会拦截异步消息,以此来尽可能的保证当接收到屏幕刷新信号时可以尽可能第一时间处理遍历绘制 View 树的工作。

(4)发完同步屏障后 scheduleTraversals() 将 performTraversals() 封装到 Runnable 里面,然后调用 Choreographer 的 postCallback() 方法。

(5)postCallback() 方法会先将这个 Runnable 任务以当前时间戳放进一个待执行的队列里,然后如果当前是在主线程就会直接调用一个native 层方法,如果不是在主线程,会发一个最高优先级的 message 到主线程,让主线程第一时间调用这个 native 层的方法。

(6)native 层的这个方法是用来向底层订阅下一个屏幕刷新信号Vsync,当下一个屏幕刷新信号发出时,底层就会回调 Choreographer 的onVsync() 方法来通知上层 app。

(7)onVsync() 方法被回调时,会往主线程的消息队列中发送一个执行 doFrame() 方法的异步消息。

(1)doFrame() 方法会去取出之前放进待执行队列里的任务来执行,取出来的这个任务实际上是 ViewRootImpl 的 doTraversal() 操作。

(8)doTraversal()中首先移除同步屏障,再会调用performTraversals() 方法根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程,在这几个流程中都会去遍历 View 树来刷新需要更新的View。

(9)等到下一个Vsync信号到达,将上面计算好的数据渲染到屏幕上,同时如果有必要开始下一帧的数据处理。

Android 屏幕刷新机制 “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解! https://blog.csdn.net/chenzhiqin20/article/details/8628952

View 刷新机制

当我们调用 View 的 invalidate 时刷新视图时,它会调到 ViewRootImp 的 invalidateChildInParent,这个方法首先会 checkThread 检查是否是主线程,然后调用其 scheduleTraversals 方法。这个方法就是视图绘制的开始,但是它并不是立即去执行 View 的三大流程,而是先往消息队列里面添加一个同步屏障,然后在往 Choreographer 里面注册一个 TRAVERSAL 的回调。在下一次 Vsync 信号到来时,会去执行 doTraversals 方法。

Choreographer 主要是用来接收 Vsync 信号,并且在信号到来时去处理一些回调事件。事件类型有四种,分别是 Input、Animation、Traversal、Commit。在 Vsync 信号到来时,会依次处理这些事件,前三种比较好理解,第四种 Commit 是用来执行组件的 onTrimMemory 函数的。Choreographer 是通过 FrameDisplayEventReceiver 来监听底层发出的 Vsync 信号的,然后在它的回调函数 onVsync 中去处理,首先会计算掉帧,然后就是 doCallbacks 处理上面所说的回调事件。

Vsync 信号可以理解为底层硬件的一个消息脉冲,它每 16ms 发出一次,它有两种方式发出,一种是 HWComposer 硬件产生,一种是用软件模拟,即 VsyncThread。不管使用哪种方式,都统一由 DispSyncThread 进行分发。

View 体系相关口水话

公众号:玩转安卓Dev

Java基础

面向对象与Java基础知识

Java集合框架

JVM

多线程与并发

设计模式

Kotlin

Android

Android基础知识

Android消息机制

Framework

View事件分发机制

Android屏幕刷新机制

View的绘制流程

Activity启动

性能优化

Jetpack&系统View

第三方框架实现原理

计算机网络

算法

其它

Clone this wiki locally