Android 面试:事件分发8连问

本文详细解释了Android系统中WindowManagerService如何通过SocketPair进行跨进程通信,以及输入事件如何从系统服务器传递到应用中的ViewRootImpl,通过InputStage责任链分发到View和Activity。重点讲述了ViewGroup的事件拦截和传递机制,包括onInterceptTouchEvent和dispatchTouchEvent方法的运用。
摘要由CSDN通过智能技术生成

这里涉及到了WindowManagerServiceBinder跨进程通信,读者不需要纠结于详细的细节

只需了解最终在SystemServer进程中,WindowManagerService根据当前的Window创建了SocketPair用于跨进程通信,同时并对App进程中传过来的InputChannel进行了注册

这之后,ViewRootImpl里的InputChannel就指向了正确的InputChannel, 作为Client端,其fdSystemServer进程中Server端的fd组成SocketPair, 它们就可以双向通信了。

然后我们App进程的主线程就会监听socket客户端,当收到消息(输入事件)后,回调NativeInputEventReceiver.handleEvent()方法,最终会走到InputEventReceiver.dispachInputEvent方法。

经过以上操作App终于拿到输入事件了,接下来就是传递到对应页面

1.4小结

关于内核处理输入事件与跨进程通信的部分一般来说不是应用开发者最关注的部分,也不是本文的重点,所以只做了概述

想要了解细节的同学可参考:Input系统—事件处理全过程

2.Touch事件到达App后怎么传递到对应页面


现在我们已经在App进程中拿到输入事件了,接下来看看事件如何分发到页面

我们接下来跟一下源码

2.1 事件回传到ViewRootImpl

//InputEventReceiver.java

private void dispatchInputEvent(int seq, InputEvent event) {

mSeqMap.put(event.getSequenceNumber(), seq);

onInputEvent(event);

}

//ViewRootImpl.java ::WindowInputEventReceiver

final class WindowInputEventReceiver extends InputEventReceiver {

public void onInputEvent(InputEvent event) {

enqueueInputEvent(event, this, 0, true);

}

}

//ViewRootImpl.java

void enqueueInputEvent(InputEvent event,

InputEventReceiver receiver, int flags, boolean processImmediately) {

adjustInputEventForCompatibility(event);

QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);

QueuedInputEvent last = mPendingInputEventTail;

if (last == null) {

mPendingInputEventHead = q;

mPendingInputEventTail = q;

} else {

last.mNext = q;

mPendingInputEventTail = q;

}

mPendingInputEventCount += 1;

if (processImmediately) {

doProcessInputEvents();

} else {

scheduleProcessInputEvents();

}

}

可以看到事件还是回到了ViewRootImpl中,可见ViewRootImpl不仅负责界面的绘制,同时负责事件的传递

2.2 第一次责任链分发

接下来走到doProcessInputEvents中,其中涉及到事件分发中的第一次责任链分发

void doProcessInputEvents() {

// Deliver all pending input events in the queue.

while (mPendingInputEventHead != null) {

QueuedInputEvent q = mPendingInputEventHead;

mPendingInputEventHead = q.mNext;

deliverInputEvent(q);

}

}

private void deliverInputEvent(QueuedInputEvent q) {

InputStage stage;

//stage赋值操作

if (stage != null) {

stage.deliver(q);

} else {

finishInputEvent(q);

}

}

abstract class InputStage {

private final InputStage mNext;

public InputStage(InputStage next) {

mNext = next;

}

public final void deliver(QueuedInputEvent q) {

if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {

forward(q);

} else if (shouldDropInputEvent(q)) {

finish(q, false);

} else {

traceEvent(q, Trace.TRACE_TAG_VIEW);

final int result;

try {

result = onProcess(q);

} finally {

Trace.traceEnd(Trace.TRACE_TAG_VIEW);

}

apply(q, result);

}

}

}

如上所示:

1.QueuedInputEvent是一种输入事件,链表结构,遍历传递给InputStage

2.InputStage是处理输入的责任链,在调用deliver时会遍历责任链传递事件

3.事件分发完成后会调用finishInputEvent,告知SystemServer进程的InputDispatcher线程,最终将该事件移除,完成此次事件的分发消费。

那么问题来了,InputStage的责任链是什么时候组件的呢?

2.3 组装责任链

我们得回到ViewRootImpl.setView方法中

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

synchronized (this) {

// Set up the input pipeline.

mSyntheticInputStage = new SyntheticInputStage();

InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);

InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,

“aq:native-post-ime:” + counterSuffix);

InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);

InputStage imeStage = new ImeInputStage(earlyPostImeStage,

“aq:ime:” + counterSuffix);

InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);

InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,

“aq:native-pre-ime:” + counterSuffix);

mFirstInputStage = nativePreImeStage;

mFirstPostImeInputStage = earlyPostImeStage;

}

}

可以看到在setView方法中,就把这条输入事件处理的责任链拼接完成了,不同的InputStage子类,通过构造方法一个个串联起来了,那这些InputStage到底干了啥呢?

  • SyntheticInputStage。综合处理事件阶段,比如处理导航面板、操作杆等事件。

  • ViewPostImeInputStage。视图输入处理阶段,比如按键、手指触摸等运动事件,我们熟知的view事件分发就发生在这个阶段。

  • NativePostImeInputStage。本地方法处理阶段,主要构建了可延迟的队列。

  • EarlyPostImeInputStage。输入法早期处理阶段。

  • ImeInputStage。输入法事件处理阶段,处理输入法字符。

  • ViewPreImeInputStage。视图预处理输入法事件阶段,调用视图viewdispatchKeyEventPreIme方法。

  • NativePreImeInputStage。本地方法预处理输入法事件阶段。

小结一下,事件到达应用端的主线程,会通过ViewRootImpl进行一系列InputStage来处理事件。这个阶段其实是对事件进行一些简单的分类处理,比如视图输入事件,输入法事件,导航面板事件等等。

我们的View触摸事件就发生在ViewPostImeInputStage阶段

final class ViewPostImeInputStage extends InputStage {

@Override

protected int onProcess(QueuedInputEvent q) {

if (q.mEvent instanceof KeyEvent) {

return processKeyEvent(q);

} else {

final int source = q.mEvent.getSource();

if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {

return processPointerEvent(q);

}

}

}

private int processPointerEvent(QueuedInputEvent q) {

final MotionEvent event = (MotionEvent)q.mEvent;

boolean handled = mView.dispatchPointerEvent(event)

return handled ? FINISH_HANDLED : FORWARD;

}

//View.java

public final boolean dispatchPointerEvent(MotionEvent event) {

if (event.isTouchEvent()) {

return dispatchTouchEvent(event);

} else {

return dispatchGenericMotionEvent(event);

}

}

1.经过层层回调会调用到mView.dispatchPointerEvent

2.我们知道ViewRootImpl中的mView就是DecorView

现在事件已经传递到了DecorView,也就是我们界面的根布局

接下来是事件在Activity,Window,DecorView中的传递

2.4 事件在Activity,Window,DecorView中的传递

//DecorView.java

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

//cb其实就是对应的Activity/Dialog

final Window.Callback cb = mWindow.getCallback();

return cb != null && !mWindow.isDestroyed() && mFeatureId < 0

? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);

}

//Activity.java

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

//PhoneWindow.java

@Override

public boolean superDispatchTouchEvent(MotionEvent event) {

return mDecor.superDispatchTouchEvent(event);

}

//DecorView.java

public boolean superDispatchTouchEvent(MotionEvent event) {

return super.dispatchTouchEvent(event);

}

可以看到事件分发经过了:DecorView -> Activity -> PhoneWindow -> DecorView

看起来是一个很奇怪的事件流转,事件从DecorView出发,最后又回到了DecorView,为什么这样做呢?

2.4.1 为什么ViewRootImpl不直接把事件交给Activity

主要是为了解藕

ViewRootImpl并不知道有Activity这种东西存在!它只是持有了DecorView。所以,不能直接把触摸事件送到Activity.dispatchTouchEvent()

2.4.2 交给Acitivity后,为什么不直接交给DecorView开始分发事件呢?

因为Activity不知道有DecorView

但是,Activity持有PhoneWindow ,而PhoneWindow当然知道自己的窗口里有些什么了,所以能够把事件派发给DecorView

Android中,Activity并不知道自己的Window中有些什么,这样耦合性就很低了,Activity不需要知道Window中的具体内容

2.5 小结

经过上述过程,事件终于到了我们熟悉的ViewGroup.dispatchTouchEvent

流程图如下所示:

3.Touch事件到达页面后内部怎样分发


下面就是我们最常用也是最常见的事件分发部分了

3.1 ViewGroup是否拦截事件

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

final boolean intercepted;

//只有ActionDown或者mFirstTouchTarget为空时才会判断是否拦截

if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

if (!disallowIntercept) {

intercepted = onInterceptTouchEvent(ev);

}

}

if (!canceled && !intercepted) {

//事件传递给子view

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

//如果子View消耗了则给mFirstTouchTarget赋值

newTouchTarget = addTouchTarget(child, idBitsToAssign);

}

}

//mFirstTouchTarget不为空时会调用dispatchTransformendTouchEvent

if (mFirstTouchTarget == null) {

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

}

}

private boolean dispatchTransformedTouchEvent(View child) {

if (child == null) {

handled = super.dispatchTouchEvent(event);

} else {

handled = child.dispatchTouchEvent(event);

}

}

从以上可以看出

1.只有当Action_Down或者mFirstTouchTarget不为空时才判断是否拦截

2.mFirstTouchTarget是个链表结构,代表某个子View消费了事件,为null则表示没有子View消费事件

3.在判断是否拦截前有个disallowIntercept字段,这个在后面事件冲突内部拦截法时会用到

4.接下来就到了onInterceptTouchEvent,ViewGroup是否拦截事件正是由这个方法控制的

3.1.2 ViewGroup拦截后会发生什么?

1.拦截之后,事件自然就不会再下发给子View

2.接下来如果mFirstTouchTargetnull,则会调用到dispatchTransformedTouchEvent,然后调用到super.dispatchTouchEvent,最终到ViewGroup.onTouchEvent

3.为什么使用mFirstTouchTarget==null来判断是否是ViewGroup处理,是因为mFirstTouchTarget==null有两种情况,一是ViewGroup拦截,二是子View没有处理事件,两种情况最后都回调到ViewGroup.onTouchEvent

通过上面的分析,我们可以得出ViewGroup拦截的伪代码:

public boolean dispatchTouchEvent(MotionEvent event) {

boolean isConsume = false;

if (isViewGroup) {

if (onInterceptTouchEvent(event)) {

isConsume = super.dispatchTouchEvent(event);

}

}

return isConsume;

}

如果是ViewGroup,会先执行到onInterceptTouchEvent方法判断是否拦截,如果拦截,则执行父类ViewdispatchTouchEvent方法。

3.1.3 ViewGroup不拦截会发生什么?

如果ViewGroup不拦截,则会传递到子View

if (!canceled && !intercepted) {

if (actionMasked == MotionEvent.ACTION_DOWN

|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)

|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

final int childrenCount = mChildrenCount;

//遍历子View

if (newTouchTarget == null && childrenCount != 0) {

for (int i = childrenCount - 1; i >= 0; i–) {

final int childIndex = getAndVerifyPreorderedIndex(

childrenCount, i, customOrder);

final View child = getAndVerifyPreorderedView(

preorderedList, children, childIndex);

//2.判断事件坐标

if (!child.canReceivePointerEvents()

|| !isTransformedTouchPointInView(x, y, child, null)) {

ev.setTargetAccessibilityFocus(false);

continue;

}

//3.传递事件

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

newTouchTarget = addTouchTarget(child, idBitsToAssign);

alreadyDispatchedToNewTouchTarget = true;

break;

}

}

}

}

}

private boolean dispatchTransformedTouchEvent(View child) {

if (child == null) {

handled = super.dispatchTouchEvent(event);

} else {

handled = child.dispatchTouchEvent(event);

}

}

如果不拦截,ViewGroup内主要做以下几件事

1.遍历当前ViewGroup的所有子View

2.判断当前View是否在当前子View的坐标范围内,不在范围内不能接收事件,直接跳过

3.利用dispatchTransformedTouchEvent,如果返回true,则通过addTouchTargetmFirstTouchTarget赋值

4.dispatchTransformedTouchEvent做的主要就是两个事,如果child不为null,则事件分发到child,否则调用super.dispatchTouchEvent,并最终返回结果

5.mFirstTouchTarget是单链表结构,记录消费链,但是在单点触控的时候这个特性没有用上,只是一个普通的TouchTarget对象

3.2 子View是否拦截

public boolean dispatchTouchEvent(MotionEvent event) {

if (onFilterTouchEventForSecurity(event)) {

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;

}

ViewdiapatchTouchEvent逻辑比较简单

1.如果设置了setOnTouchListener并且返回为true,那么onTouchEvent就不再执行

2.否则执行onTouchEvent,我们常用的OnClickListenr就是在onTouchEvent里触发的

所以默认情况下会直接执行onTouchEvent,如果我们设置了setOnClickListener或者setLongClickListener,都会正常触发

3.2.1 如果子View消费事件会怎么样?

上面说了,如果子View消费事件,即dispatchTouchEvent方法返回true

表示这个事件我处理了,那么事件从此结束,ViewGroupdispatchTouchEvent也返回true

最后回到ActivitydispatchTouchEvent,也是直接返回true

//Activity.java

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

小结:如果子View消费事件的话,事件就此结束了

3.2.2 如果子View不消费事件会怎么样?

View不拦截事件,那么mFirstTouchTarget就为null,退出循环后,调用了dispatchTransformedTouchEvent方法。

if (mFirstTouchTarget == null) {

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

}

小结一下:

1.子View不拦截事件,就回调到了dispatchTransformedTouchEvent

2.然后就调到了super.dispatchTouchEvent

3.那么接下来ViewGroup就跟子View的逻辑一样了,默认执行onTouchEvent,如果设置了setOnTouchLister则执行onTouch

3.3 如果ViewGroup与子View都不拦截会怎么样

如果ViewGroup与子View都不拦截,即mFirstTouchTarget == null,dispatchTouchEvent也返回false

再看看Activity的源码

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

答案很明显:会执行ActivityonTouchEvent方法

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

可以看出,笔者的工作学习模式便是由以下 「六个要点」 组成:

❝ 多层次的工作/学习计划 + 番茄工作法 + 定额工作法 + 批处理 + 多任务并行 + 图层工作法❞

希望大家能将这些要点融入自己的工作学习当中,我相信一定会工作与学习地更富有成效。

下面是我学习用到的一些书籍学习导图,以及系统的学习资料。每一个知识点,都有对应的导图,学习的资料,视频,面试题目。

**如:我需要学习 **Flutter的知识。(大家可以参考我的学习方法)

  • Flutter 的思维导图(无论学习什么,有学习路线都会事半功倍)

  • Flutter进阶学习全套手册

  • Flutter进阶学习全套视频

大概就上面这几个步骤,这样学习不仅高效,而且能系统的学习新的知识。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

…(img-yRqL46W0-1711773598784)]

[外链图片转存中…(img-QJeouM0Z-1711773598784)]

[外链图片转存中…(img-iD1gg540-1711773598785)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

可以看出,笔者的工作学习模式便是由以下 「六个要点」 组成:

❝ 多层次的工作/学习计划 + 番茄工作法 + 定额工作法 + 批处理 + 多任务并行 + 图层工作法❞

希望大家能将这些要点融入自己的工作学习当中,我相信一定会工作与学习地更富有成效。

下面是我学习用到的一些书籍学习导图,以及系统的学习资料。每一个知识点,都有对应的导图,学习的资料,视频,面试题目。

**如:我需要学习 **Flutter的知识。(大家可以参考我的学习方法)

  • Flutter 的思维导图(无论学习什么,有学习路线都会事半功倍)

[外链图片转存中…(img-RrYGL6iD-1711773598785)]

  • Flutter进阶学习全套手册

[外链图片转存中…(img-KRATtcBj-1711773598785)]

  • Flutter进阶学习全套视频

[外链图片转存中…(img-Zicvcrxh-1711773598785)]

大概就上面这几个步骤,这样学习不仅高效,而且能系统的学习新的知识。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值