Android事件分发机制

那接下来,我们走进它的源码一窥究竟。

为了让思路更简洁清晰,不受那些细节的影响,以下代码只摘录与本文最相关的部分,甚至你可以直接当成伪代码来看。

我们就从setContentView()作为入口:

#Activity

private Window mWindow

public void setContentView(@LayoutRes int layoutResID) {

getWindow().setContentView(layoutResID);

initWindowDecorActionBar();

}

public Window getWindow() {

return mWindow;

}

可以看到它又通过 getWindow()setContentView,这个mWindow具体的实例对象是PhoneWindow,它会在Activity 的启动过程,通过attch()创建。

#ActivityThread

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

activity.attach(…);

}

#Activity

attch(…){

mWindow = new PhoneWindow(this, window, activityConfigCallback);

}

接下来走进PhoneWindowsetContentView()

#PhoneWindow

public void setContentView(int layoutResID) {

if (mContentParent == null) {

installDecor();

}

mLayoutInflater.inflate(layoutResID, mContentParent);

}

mLayoutInflater.inflate()函数我们就很熟悉了,它会把布局layoutResID填充到根布局mContentParent里面

mContentParent这个view是什么呢,怎么来的呢?可以从installDecor()函数去看:

#PhoneWindow

private void installDecor() {

if (mDecor == null) {

//生成DecorView

mDecor = generateDecor(-1);

}

if (mContentParent == null) {

//生成mContentParent

mContentParent = generateLayout(mDecor);

}

}

//生成DecorView

protected DecorView generateDecor(int featureId) {

return new DecorView(…)

}

//DecorView是继承FrameLayout的view

public class DecorView extends FrameLayout{}

//生成mContentParent

protected ViewGroup generateLayout(DecorView decor) {

TypedArray a = getWindowStyle();

int layoutResource;

//会根据一些设置去配置布局

if ((…) {

layoutResource = …

} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {

//比如这里我们常设置不需要Title的Activity就会走这里 requestWindowFeature(Window.FEATURE_NO_TITLE);

layoutResource = R.layout.screen_title;

}else{

//如果没有设置的话

layoutResource = R.layout.screen_simple;

}

//将layoutResource加载到DecorView去

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

//ID_ANDROID_CONTENT = com.android.internal.R.id.content

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

return contentParent;

}

最终返回结果contentParent,它是id为R.id.content的ViewGroup。那这个id从哪个布局获取到的呢,可以猜测出默认布局R.layout.screen_simple会有这个id:

<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”

android:layout_width=“match_parent”

android:layout_height=“match_parent”

android:fitsSystemWindows=“true”

android:orientation=“vertical”>

<ViewStub android:id=“@+id/action_mode_bar_stub”

android:inflatedId=“@+id/action_mode_bar”

android:layout=“@layout/action_mode_bar”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:theme=“?attr/actionBarTheme” />

<FrameLayout

android:id=“@android:id/content”

android:layout_width=“match_parent”

android:layout_height=“match_parent”

android:foregroundInsidePadding=“false”

android:foregroundGravity=“fill_horizontal|top”

android:foreground=“?android:attr/windowContentOverlay” />

从布局中可以看到,contentParent是个idcontentFrameLayout

到这里我们就可以得到结论: 我们在ActivityonCreate()方法里常写的setContentView(layoutResID)最终会填充到FrameLayout的布局中去。而这个FrameLayout是放在一个竖直方向的LinearLayout里面。

那这整个以LinearLayout为最外层的layout要放在哪里呢?

//将layoutResource加载到DecorView去

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

public class DecorView extends FrameLayout

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {

final View root = inflater.inflate(layoutResource, null);

addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

}

}

从这段代码可以看出 ,contentParent最终会通过addView方法填充到DecorView中去。

到这里可以小结一下:在Activity里设置的布局会添加到名为contentParentview上,而contentParent会添加到DecorView中,而DecorViewPhoneWindow下的View,所以它也顺理成章成为了整个View树的顶级View。从这里也可以得出一个结论:Activity它并负责视图控制,真正控制试图的是PhoneWindow

可以思考一个问题:这样看没有Activity的存在也是可以的,那Activity被设计的目的是什么呢?

为了可以更直观的展示上面的结论,上图:

提这个前置知识最重要有两个目的:

  1. 对整个View树的构建过程有个了解

  2. View树的顶级View是DecorView

知道这两点后可以帮助我们更容易的理解后面的核心内容。

事件分发整体的设计


事件分发的源头往往是我们触摸屏幕(包含手指、笔、鼠标等),它最开始是一个物理层面的触摸输入事件,然后通过一系列转换过程到我们应用上层。作为应用层开发者,更直接关注到的肯定还是应用层面的代码。

那假设这个触摸输入事件已经到我们的应用层了,那我们从应用层角度来设计的话,会如何设计?

我们是否只要定义这样一个类MotionInputEvent去接收事件并处理就可以了?看命名还挺贴切的。其实仔细想想,除了触摸输入事件外,常用的还有键盘输入,所以只定义这样一个类从设计上是不合理的,没有考虑到它的可扩展性。

因此定义一个输入事件抽象层InputEvent,让触摸输入事件MotionEvent、键盘输入KeyEvent都继承该抽象类,这才是一个合理的设计。看看源码确实是这么设计的:

#package android.view;

public abstract class InputEvent implements Parcelable{}

public final class MotionEvent extends InputEvent implements Parcelable {}

public class KeyEvent extends InputEvent implements Parcelable {}

既然有了这两种输入事件类型,肯定还需要一个地方管理它们:在接收到输入事件的时候进行管理,然后再分发给具体的输入事件类来处理。这个类就是系统输入管理器InputManagerInputManager作为管理分发InputEvent的地方,那它是在哪里被创建出来的呢?

我们知道手机启动后,手机就要响应我们的输入事件,所以可以猜测出在系统启动时,和它相关的某个类应该就要创建出来。查看frameworks层的SystemServer类,找到那个最直接相关的类,发现是 InputManagerService ,看着是不是有点熟悉。看它长得就像我们熟知的 ActivityManagerServiceWindowManagerService一样,所以它在SystemServer启动时应该就会被创建出来。

#frameworks/base/services/java/com/android/server/SystemServer.java

public final class SystemServer {

private void run() {

startOtherServices()

}

private void startOtherServices() {

InputManagerService inputManager = null

inputManager = new InputManagerService(context);

//这里可以看出,InputManagerService和WindowManagerService有紧密的联系,InputManagerService的实例直接传给了WindowManagerService的main方法

wm = WindowManagerService.main(context, inputManager, !mFirstBoot, mOnlyCore,

new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);

inputManager.start();

}

}

#frameworks/base/services/core/java/com/android/server/input/InputManagerService.java

public class InputManagerService extends IInputManager.Stub

implements Watchdog.Monitor {

public InputManagerService(Context context) {

mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());

}

//调用native方法初始化InputManagerService

private static native long nativeInit(InputManagerService service,

Context context, MessageQueue messageQueue);

}

有意思的是,从源码可以看到InputManagerServiceWindowManagerService居然建立了联系,这是我们一开始没有想到的。不过细想琢磨一下,如果对WindowManagerService 熟悉的话,会发现这样的设计是理所当然的:

WindowManagerService是窗口的大主管(下面简称WMS),它记录了当前系统中所有窗口的完整信息,所以只有它才能判断出要把输入的事件投递给具体的某个应用进程进行处理。

当然具体怎么传递的就涉及细节。不用理会它们,在考虑整体设计的时候,具体细节都可暂时跳过,不能被它们蒙蔽了双眼,阻碍了前进的脚步。 这些细节点,可在事后找时间逐一击破它们。

到这里我们可以小结一下:在系统启动时,SystemServer会启动窗口管理服务WindowManagerServiceWindowManagerService在启动的时候就会通过InputManagerService,启动Native层的InputManager来接收硬件层的输入事件。接收到输入事件后,WindowManagerService会经过判断把输入事件分发给某个具体的应用进程。

WMS不仅是是窗口的大主管,还是InputEvent的派发者

那么现在的问题就转变成:WMS分发输入事件InputEvent后,应用进程如何接收的了?这里的应用进程可以理解成是一个应用层级的窗口Window。因为事件输入的目的地是应用层级的窗口Window

我们可以想到的是在WMS应用窗口Window中间肯定需要一个纽带或者说是中介,去衔接这两者。那这个纽带是谁呢?

对View绘制比较了解的人应该很熟悉ViewRootImpl。View的测量、布局、绘制都由它控制。它作为整个View树的根部,是View树正常运作的基石。后面的分析过程也会提到这一点的。

于此同时,ViewRootImpl就是WMS应用窗口Window建立通信的纽带。既然是纽带,那么必然有两者的依赖,所以它的创建过程就非常重要了,按理说是可以从中找到依据的。

ViewRootImpl的创建


ViewRootImpl是在哪里被创建的呢?这就要从handleResumeActivity流程看起

#ActivityThread

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {

r.window = r.activity.getWindow();

ViewManager wm = a.getWindowManager(); //这里的wm实例对象就是WindowManagerImpl

WindowManager.LayoutParams l = r.window.getAttributes();

wm.addView(decor, l);

}

wm.addViewwm的实例对象就是WindowManagerImpl,其中的参数decorDecorView对象,在前置知识ViewTree的创建过程中已经提过了。

所以接着看WindowManagerImpladdView方法做了什么?

#WindowManagerImpl

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

@Override

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {

applyDefaultToken(params);

mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);

}

又调用到 WindowManagerGlobaladdView方法

#WindowManagerGlobal

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {

ViewRootImpl root;

root = new ViewRootImpl(view.getContext(), display); //创建了ViewRootImpl对象

mViews.add(view); //记录DecorView

mRoots.add(root); //记录ViewRootImpl

Params.add(wparams); //记录WindowManager的LayoutParams

root.setView(view, wparams, panelParentView);

}

到这里可以先小结一下:在ActivityThread.handleResumeActivity()流程中, 通过WindowManager(WindowManagerImpl)addView() 实现了ViewRootImpl的创建。 此时我们应用层窗口 Window就和ViewRootImpl就建立了关联。

从代码中可以看到,除了ViewRootImpl的创建还会把构建出来的ViewRootImplDecorViewWindowManager.LayoutParams 记录下来,用几个数组分别存储,它们是一一对应的。

那么,现在应用层窗口 Window就和ViewRootImpl就建立了关联,还剩一个问题是 WMS 和 ViewRootImpl怎么建立关联的呢?

继续往下看源码,进入ViewRootImpl 的 setView 方法,我们就能找到答案了。看看它做了什么?

#ViewRootImpl

final IWindowSession mWindowSession

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

// 控制测量(measure)、布局(layout)、绘制(draw)的开始

requestLayout();

//包括了发送和接收消息的功能封装

mInputChannel = new InputChannel();

//通过Binder调用,进入系统进程的Session,调用WMS的addWindow方法

res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,

getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,

mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,

mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,

mTempInsets);

//创建事件输入处理接收者

mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,

Looper.myLooper());

}

mWindowSession.addToDisplay()函数是添加窗口流程,对应的服务端就是WMS,而WMS又是个系统进程,所以这是个Binder跨进程调用方法,最终调用的是WMSaddWindow方法。而参数mInputChannel,它包括了发送和接收消息的功能封装。

至此这里已经验证了我们想要的结果了,ViewRootImpl确实是应用层级WindowWMS的建立了通信的纽带。

小插入:前面提到ViewRootImpl是View绘制的根基。为什么这么说?

可以从setViewrequestLayout()看出。在requestLayout()方法,它主要做了两件事情:

  1. 检查线程

  2. 开始测量(measure)、布局(layout)、绘制(draw)

#ViewRootImpl

@Override

public void requestLayout() {

if (!mHandlingLayoutInLayoutRequest) {

checkThread();

mLayoutRequested = true;

scheduleTraversals();

}

}

void scheduleTraversals() {

if (!mTraversalScheduled) {

mTraversalScheduled = true;

//在同步信号过来的时候, mTraversalRunnable的run函数将调用

mChoreographer.postCallback(

Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

}

}

final class TraversalRunnable implements Runnable {

@Override

public void run() {

doTraversal();

}

}

void doTraversal() {

performTraversals();

}

private void performTraversals() {

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)

performLayout(lp, mWidth, mHeight);

performDraw();

}

应用层级的事件分发流程


现在WMS和应用层窗口了已经有了通信的条件。输入事件可以从底层硬件分发到应用层窗口了。发是发过来了,可还需要一个接收者呀。如果你细心的话,或许会在上面代码里有所发现。在setView方法的最后,创建了一个WindowInputEventReceiver对象。它的职责就是接受输入事件的。

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

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

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

img

img

img

img

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

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

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

最后

简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。

选定你想去的几家公司后,先去一些小的公司练练,学习下面试技巧,总结下,也算是熟悉下面试氛围,平时和同事或者产品PK时可以讲得头头是道,思路清晰至极,到了现场真的不一样,怎么描述你所做的一切,这绝对是个学术性问题!

面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。

金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。

选定你想去的几家公司后,先去一些小的公司练练,学习下面试技巧,总结下,也算是熟悉下面试氛围,平时和同事或者产品PK时可以讲得头头是道,思路清晰至极,到了现场真的不一样,怎么描述你所做的一切,这绝对是个学术性问题!

面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。

金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-IItA7Wic-1713788933911)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值