Android 事件分发机制,总结到位
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);
}
接下来走进PhoneWindow
的setContentView()
#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" />
</LinearLayout>
从布局中可以看到,contentParent
是个id
为content
的FrameLayout
。
到这里我们就可以得到结论: 我们在Activity
中onCreate()
方法里常写的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 里设置的布局会添加到名为contentParent
的view
上,而contentParent
会添加到DecorView
中,而DecorView
是PhoneWindow
下的View
,所以它也顺理成章成为了整个 View 树的顶级 View。从这里也可以得出一个结论:Activity 它并负责视图控制,真正控制试图的是PhoneWindow
。
可以思考一个问题:这样看没有 Activity 的存在也是可以的,那 Activity 被设计的目的是什么呢?
为了可以更直观的展示上面的结论,上图:
提这个前置知识最重要有两个目的:
对整个 View 树的构建过程有个了解
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 {}
既然有了这两种输入事件类型,肯定还需要一个地方管理它们:在接收到输入事件的时候进行管理,然后再分发给具体的输入事件类来处理。这个类就是系统输入管理器InputManager
。InputManager
作为管理分发 InputEvent 的地方,那它是在哪里被创建出来的呢?
我们知道手机启动后,手机就要响应我们的输入事件,所以可以猜测出在系统启动时,和它相关的某个类应该就要创建出来。查看 frameworks 层的SystemServer
类,找到那个最直接相关的类,发现是 InputManagerService
,看着是不是有点熟悉。看它长得就像我们熟知的 ActivityManagerService
、WindowManagerService
一样,所以它在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);
}
有意思的是,从源码可以看到InputManagerService
和WindowManagerService
居然建立了联系,这是我们一开始没有想到的。不过细想琢磨一下,如果对WindowManagerService
熟悉的话,会发现这样的设计是理所当然的:
WindowManagerService
是窗口的大主管(下面简称WMS
),它记录了当前系统中所有窗口的完整信息,所以只有它才能判断出要把输入的事件投递给具体的某个应用进程进行处理。
当然具体怎么传递的就涉及细节。不用理会它们,在考虑整体设计的时候,具体细节都可暂时跳过,不能被它们蒙蔽了双眼,阻碍了前进的脚步。 这些细节点,可在事后找时间逐一击破它们。
到这里我们可以小结一下:在系统启动时,SystemServer
会启动窗口管理服务WindowManagerService
,WindowManagerService
在启动的时候就会通过InputManagerService
,启动Native
层的InputManager
来接收硬件层的输入事件。接收到输入事件后,WindowManagerService
会经过判断把输入事件分发给某个具体的应用进程。
WMS
不仅是是窗口的大主管,还是InputEvent
的派发者
那么现在的问题就转变成:WMS
分发输入事件InputEvent
后,应用进程如何接收的了?这里的应用进程可以理解成是一个应用层级的窗口Window
。因为事件输入的目的地是应用层级的窗口Window
。
我们可以想到的是在WMS
和应用窗口Window
中间肯定需要一个纽带或者说是中介,去衔接这两者。那这个纽带是谁呢?
对 View 绘制比较了解的人应该很熟悉ViewRootImpl
。View 的测量、布局、绘制都由它控制。它作为整个 View 树的根部,是 View 树正常运作的基石。后面的分析过程也会提到这一点的。
于此同时,ViewRootImpl
就是WMS
和应用窗口Window
建立通信的纽带。既然是纽带,那么必然有两者的依赖,所以它的创建过程就非常重要了,按理说是可以从中找到依据的。
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.addView
的wm
的实例对象就是WindowManagerImpl
,其中的参数decor
是DecorView
对象,在前置知识 ViewTree 的创建过程中已经提过了。
所以接着看WindowManagerImpl
的addView
方法做了什么?
#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);
}
又调用到 WindowManagerGlobal
的addView
方法
#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
的创建还会把构建出来的ViewRootImpl
、DecorView
、WindowManager.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
跨进程调用方法,最终调用的是WMS
的addWindow
方法。而参数mInputChannel
,它包括了发送和接收消息的功能封装。
至此这里已经验证了我们想要的结果了,ViewRootImpl
确实是应用层级Window
和WMS
的建立了通信的纽带。
小插入:前面提到 ViewRootImpl 是 View 绘制的根基。为什么这么说?
可以从setView
的requestLayout()
看出。在requestLayout()
方法,它主要做了两件事情:
检查线程
开始测量(measure)、布局(layout)、绘制(draw)
#ViewRootImpl
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
评论