中建立InputChannel,WindowInputEventReceiver的创建)
setView()中的InputChannel接收逻辑我们先找到ViewRootImpl.setView()这个方法。它是在ActivityThread.handleResumeActivity()里被调用的传入了宿主View和对应的WindowManager.LayoutParams。关键代码在这里public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView null) { mView view; ... // 1. 创建InputChannel InputChannel inputChannel null; if ((mWindowAttributes.inputFeatures WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) 0) { inputChannel new InputChannel(); } ... // 2. 通过Binder调用WMS添加窗口 res mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel, mTempInsets, mTempControls); ... // 3. 创建WindowInputEventReceiver if (mInputChannel ! null) { if (mInputQueueCallback ! null) { mInputQueue new InputQueue(); mInputQueueCallback.onInputQueueCreated(mInputQueue); } mInputEventReceiver new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); } ... } } }嗯这里有三步我一个个拆开讲。第一步创建InputChannel对象注意看这里new出来的InputChannel只是一个空壳。它还没有和任何底层的文件描述符绑定。真正的通道建立是在WMS那边完成的。核心要点应用端new InputChannel()只是分配了一个Java对象真正的SocketPair创建在WMS的addWindow()中。WMS会把服务端通道填进这个对象然后通过Binder传回来。我个人习惯在分析这类代码时先搞清楚「谁创建、谁填充、谁使用」。这里就是应用端创建空InputChannel对象WMS端创建SocketPair填充服务端和客户端通道应用端接收填充好的InputChannel开始监听第二步mWindowSession.addToDisplay()这个Binder调用我上一章详细讲过。它会走到WMS的addWindow()方法里完成InputChannel的创建和填充。调用完成后mInputChannel这个成员变量就不再是空的了。它里面包含了客户端端的文件描述符。你可以通过mInputChannel.getFd()拿到它——不过说实话我们一般不会直接操作fd而是通过InputEventReceiver来封装。小提示我在项目中遇到过一个问题——某些定制ROM在addToDisplay返回后mInputChannel仍然是空的。后来发现是WMS的InputChannel创建逻辑被改了。排查时可以用adb shell dumpsys input查看窗口的InputChannel状态确认是否注册成功。第三步创建WindowInputEventReceiver这一步是关键中的关键。我们来看WindowInputEventReceiver的构造final class WindowInputEventReceiver extends InputEventReceiver { public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); } ... }它继承自InputEventReceiver。而InputEventReceiver的构造方法会调用native层public InputEventReceiver(InputChannel inputChannel, Looper looper) { ... mReceiverPtr nativeInit(new WeakReferenceInputEventReceiver(this), inputChannel, looper.getQueue()); }这个nativeInit()方法在C层做了三件重要的事创建一个NativeInputEventReceiver对象把这个对象和InputChannel的fd绑定把fd注册到Looper的epoll中开始监听可读事件说白了就是把InputChannel的fd挂到了主线程的Looper上。一旦有触摸事件从WMS发过来fd就会变成可读状态Looper就会回调NativeInputEventReceiver的handleEvent()方法。注意WindowInputEventReceiver是在主线程创建的所以它使用的是主线程的Looper。这意味着所有输入事件的分发都是在主线程上执行的。如果你在主线程做耗时操作触摸事件就会卡住——嗯这就是为什么「应用无响应」时屏幕点不动的原因之一。InputChannel的传递链路总结为了让你看得更清楚我画了个简化的流程表步骤所在位置做了什么1ViewRootImpl.setView()new InputChannel() 创建空对象2WMS.addWindow()创建SocketPair填充InputChannel3ViewRootImpl.setView()接收填充后的InputChannel4new WindowInputEventReceiver()创建接收器注册到Looper5NativeInputEventReceiver监听fd事件到来时回调我曾经踩过的坑讲到这里我想分享一个真实案例。曾经我优化一个视频播放器的触摸响应延迟。发现从手指触屏到MotionEvent到达View总是多出2-3ms的延迟。排查了很久最后发现是WindowInputEventReceiver的创建时机问题。原来在那个项目中有人在Activity.onCreate()里手动调了setContentView()之后又异步做了一些初始化操作。这导致ViewRootImpl.setView()被延迟执行了。而InputChannel的建立和Receiver的注册都卡在了setView()里。解决方案很简单确保setContentView()之后尽快让ViewRootImpl完成setView()。说白了就是不要在onCreate()里做太多耗时操作尤其是那些在setContentView()之后才执行的任务。小结这一章我们重点看了ViewRootImpl.setView()中InputChannel的建立和WindowInputEventReceiver的创建。记住三个关键点InputChannel是WMS创建的应用端只是接收WindowInputEventReceiver把fd挂到主线程Looper上所有触摸事件的分发都在主线程执行