深入理解 Android 控件

  • 2017-07-01
  • 20,319
  • 13

概述

本篇文章主要通过源码讲述 Android 控件系统,包括输入事件是如何产生的, View 是如何绘制的,输入事件是如何传递给 View 的,Window token 与 type 之间的联系等。整个系统比较复杂,每个部分只能点到为止,有兴趣可以继续深入,主要是让读者对 Android 控件系统有一个大体的认识。

例子

下面是创建 Window 并显示 View 最简单的一个例子:


public class WindowService extends Service {
    WindowManager windowManager;
    ImageView imageView;
    public WindowService() {
    }
    @Override
    public void onCreate() {
        super.onCreate();
        windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
    }
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (imageView == null) {
            installWindow();
        }
        return START_STICKY;
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        if (imageView != null) {
            windowManager.removeView(imageView);
        }
    }
    private void installWindow() {
        imageView = new ImageView(this.getBaseContext());
        imageView.setImageResource(R.mipmap.ic_launcher_round);
        final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.format = PixelFormat.TRANSPARENT;
        layoutParams.width = 200;
        layoutParams.height = 200;
        layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE ;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 
                             |WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 
                             | WindowManager.LayoutParams.FLAG_FULLSCREEN 
                             | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
        windowManager.addView(imageView, layoutParams);
        imageView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                layoutParams.x = (int) event.getRawX() - 100;
                layoutParams.y = (int) event.getRawY() - 100;
                windowManager.updateViewLayout(imageView, layoutParams);
                return true;
            }

        });
    }
}

只要启动这个 Service,就会创建一个随手指移动的悬浮窗,关闭 Service 会移除悬浮窗。另外由于这里设置了 Window 的 type 为 TYPE_PHONE, 所以需要 SYSTEM_ALERT_WINDOW 权限,也可以使用 TYPE_TOAST。

Window type 与 token

这里简单的说一下 Window type 对窗口创建的影响,WindowManager.LayoutParams 还有一个 token 的属性,token 标志着一组窗口,
比如 Activity 启动的时候会向 WMS 注册一个 type 为 TYPE_APPLICATION 的 AppWindowToken,然后 Activity 就可以拿着这个 token 创建类型为 TYPE_APPLICATION 的窗口,Dialog 就必须传 Activity 的 Context,这是因为 Dialog 的窗口类型为 TYPE_APPLICATION。

WMS 中会有一个 Map 保存所有的 WindowToken,其中 key 为 WindowManager.LayoutParams 中设置的 token,可以是任意一个 IBinder 对象,value 为 WindowToken,WindowToken 是由 WMS 创建的,同时会保存应用传过来的 token 对象。

WindowToken 又分为显式创建和隐式创建,如果 Window 的 type 不是 APPLICATION_WINDOW,TYPE_INPUT_METHOD, TYPE_WALLPAPER, TYPE_DREAM 中的任意一个, 那么即使未注册 token,WMS 也会隐式创建一个 WindowToken 保存在 Map 中,其他类型必须通过 WMS 显式注册 token。对于 type 为 APPLICATION_WINDOW 的窗口, token 必须为 WindowToken 的子类 AppWindowToken,其他类型需要与 Token 注册的类型一一对应。

回到 WindowManger,提供了3操作方法,分别是新增,修改和移除:


windowManager.addView(imageView, layoutParams);
windowManager.updateViewLayout(imageView, layoutParams);
windowManager.removeView(imageView)

Android 控件系统相当复杂,但是 Android 工程师们为我们高度封装了 WindowManager,简洁到只有三个方法。

从 WindowManager 开始

WindowManager 是一个继承于 ViewManager 的接口,实际类型是WindowManagerImpl,事实上 ViewGroup 也继承于 ViewManager 接口,可见 WindowManagerImpl 的功能与 ViewGroup 类似,提供了一个显示 View 的容器。
可以通过下面的方法拿到 WindowManager 的实例:


windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);

我们知道通过 addView 方法可以将一个 View 显示出来,下面是位于 WindowManagerImpl 中 addView 方法的实现:


@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
     applyDefaultToken(params);
     mGlobal.addView(view, params, mDisplay, mParentWindow);
}

直接交给了 mGlobal, 它是进程唯一的,类型为 WindowManagerGlobal, 继续看 WindowManagerGlobal 的 addView 方法,截取了主要逻辑:


public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    ViewRootImpl root;
    View panelParentView = null;
    synchronized (mLock) {
        int index = findViewLocked(view, false);
        if (index >= 0) {
            if (mDyingViews.contains(view)) {
                mRoots.get(index).doDie();
            } else {
                throw new IllegalStateException("View " + view + " has already been added to the window manager.");
            }
        }
        if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            final int count = mViews.size();
            for (int i = 0; i < count; i++) {
                if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                    panelParentView = mViews.get(i);
                }
            }
        }
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }

    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

先是查看 View 是否已经被添加过,如果已经被添加过则抛出异常,与 ViewGroup 添加子 View 的行为一致,只能添加一次。
然后查看 Window 设置的 type 是否是位于 FIRST_SUB_WINDOW 到 LAST_SUB_WINDOW,这类 Window 被视为子窗口, 子窗口的 token 必须与其父窗口一致。所以接来下的代码就是找出子窗口的父窗口保存为 panelParentView。
然后实例化了一个 ViewRootImpl 对象,并且将 View, ViewRootImpl, LayoutPamrms 分别保存至各自的列表中以便后续查询。
最后调用 ViewRootImpl 的 setView 方法,ViewRootImpl 是 Android 上层应用界面绘制的核心类。

深入 ViewRootImpl

ViewRootImpl 的构造方法接受两个参数,一个是 Context,一个是 Display。构造 ViewRootImpl 的时候会初始化很多成员变量,需要注意的有:
1. sWindowSession,单例的 WindowSession 类型,与系统进程通讯的关键,客户端向WMS请求窗口操作的中间代理:


mWindowSession = WindowManagerGlobal.getWindowSession();

下面是 WindowManagerGlobal 中获取 WindowSession 的代码,可以看到是由 WindowManagerService 产生,并且与 InputMethodManager 相关,以支持接收输入事件。


public static IWindowSession getWindowSession() {
    synchronized (WindowManagerGlobal.class) {
        if (sWindowSession == null) {
            try {
                InputMethodManager imm = InputMethodManager.getInstance();
                IWindowManager windowManager = getWindowManagerService();
                sWindowSession = windowManager.openSession(
                        new IWindowSessionCallback.Stub() {
                            @Override
                            public void onAnimatorScaleChanged(float scale) {
                                ValueAnimator.setDurationScale(scale);
                            }
                        },
                        imm.getClient(), imm.getInputContext());
            } catch (RemoteException e) {
                Log.e(TAG, "Failed to open window session", e);
            }
        }
        return sWindowSession;
    }
}

2. mThread,保存当前的线程,即UI线程:


mThread = Thread.currentThread();

用于校验操作 UI 的线程是否正确:


void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

3. mWindow,一个 Binder 对象的服务端,IWindow.Stub 的子类,标志这个窗口的ID,同时用于接收回调:


mWindow = new W(this);

上面提到 WMS 会保存 WindowToken 标志一组窗口组,那么这里的 mWindow 会作为这个窗口的唯一标识,WMS 中还有一个 Map 用于保存所有的 Window,其中 key 为 mWindow.asBinder(),value 为 WindowState 对象, 表示一个窗口的所有属性。
4. mSurface, Surface 实例,用于绘制界面,但是刚创建的时候是无效的,后续 WindowManagerService 会为其分配对应 native 层的对象。上面的 mWindow 并不是真正意义上的窗口,是一个窗口 ID,并承载着远程回调的作用,真正绘制界面的是 Surface。
5. mChoreographer,功能类似于 Handler,区别在于 Handler 会在 Looper 所在线程空闲的时候执行消息,执行时机不可预测,Choreographer 会接收显示系统的 VSync 信号,在下一个 frame 渲染时执行这些操作,更适合于 UI 渲染与动画显示。


mChoreographer = Choreographer.getInstance();

使用方法和 Handler 很类似:


mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

上述代码中会在下次 VSync 信号到来的时候执行 Runnable mTraversalRunnable.

介绍完主要的参数之后,开始看 ViewRootImpl 的 setView 方法,节选了主要代码:


public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            mWindowAttributes.copyFrom(attrs);
            attrs = mWindowAttributes;

            int res; /* = WindowManagerImpl.ADD_OKAY; */

            // Schedule the first layout -before- adding to the window
            // manager, to make sure we do the relayout before receiving
            // any other events from the system.
            requestLayout();
            if ((mWindowAttributes.inputFeatures
            & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                mInputChannel = new InputChannel();
            }
            try {
                mOrigWindowType = mWindowAttributes.type;
                mAttachInfo.mRecomputeGlobalAttributes = true;
                collectViewAttributes();
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);
            } catch (RemoteException e) {
                mAdded = false;
                mView = null;
                mAttachInfo.mRootView = null;
                mInputChannel = null;
                mFallbackEventHandler.setView(null);
                unscheduleTraversals();
                setAccessibilityFocus(null, null);
                throw new RuntimeException("Adding window failed", e);
            } finally {
                if (restore) {
                    attrs.restore();
                }
            }
            if (res < WindowManagerGlobal.ADD_OKAY) {
                // 错误处理
            }
            if (mInputChannel != null) {
                if (mInputQueueCallback != null) {
                    mInputQueue = new InputQueue();
                    mInputQueueCallback.onInputQueueCreated(mInputQueue);
                }
                mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper());
            }

            view.assignParent(this);
        }
    }
}

上面的代码主要做了以下几件事:

  1. 将 view 保存成全局变量,保存 LayoutParams 到 mWindowAttributes
  2. 再安排第一次遍历:requestLayout(),在一次遍历中会进行测量、布局、向 WMS 申请绘图表面(Surface)进行绘制。
  3. 添加窗口 addToDisplay,此时 mInputChannel 可以接受输入事件了,但是 Surface 还不能进行绘制。
  4. 创建 WindowInputEventReceiver 用于接收输入事件

在上面的步骤中有两个要点,一个绘制部分,一个是监听输入事件部分。正是这两部分实现了 Android 系统丰富多彩的 UI。

控件绘制:


public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

requestLayout 实际上调用的是 scheduleTraversals 方法,scheduleTraversals 方法中先在 mHandler 中插入了一个消息屏障,插入消息屏障之后,mHandler 便会暂停处理同步消息,在调用 performTraversals 之前会移除屏障,这样做可以防止在绘制开始之前接受事件回调。对消息屏障有疑问的可以看之前的文章《深入理解MessageQueue》
接着在下次VSync信号到来的时候会执行 Runnable:mTraversalRunnable, 实际上执行的是 performTraversals(),performTraversals() 是控件系统的心跳,窗口属性变化,尺寸变化、重绘请求等都会引发 performTraversals()的调用。View类绘制的核心方法 onMeasure()、onLayout()以及onDraw()等回调都会在 performTraversals() 的执行过程中执行。

预测量阶段

performTraversals 方法中会调用 measureHierarchy 进行协商测量:


private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;
    boolean goodMeasure = false;
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        final DisplayMetrics packageMetrics = res.getDisplayMetrics();
        res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
        int baseSize = 0;
        if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
		// 获取系统配置项中获取限制宽度
            baseSize = (int)mTmpValue.getDimension(packageMetrics);
        }
        if (baseSize != 0 && desiredWindowWidth > baseSize) {
            childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
		// 通过默认最大宽度进行第一次测量
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
		// 查看是否含有标志位 MEASURED_STATE_TOO_SMALL
            if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                goodMeasure = true;
            } else {
                // 扩大限制宽度
                baseSize = (baseSize+desiredWindowWidth)/2;
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
		// 第二次测量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    goodMeasure = true;
                }
            }
        }
    }
    if (!goodMeasure) {
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
	// 如果还是太小,放弃限制使用最大宽度进行第三次测量,此次测量不管是否满意
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            windowSizeMayChange = true;
        }
    }
    return windowSizeMayChange;
}

在上述方法中最多会测量三次,最少测量一次。多次测量的情况主要出现在控件树中有某个控件测量结果高2位带上了标志位 MEASURED_STATE_TOO_SMALL,应尽量避免带上该标志位。
实际测量的方法位于performMeasure:


private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

最终调用了我们熟悉的View.measure方法,也就是这里控件树根节点的 messure 方法,开始遍历控件树的测量。
不出意外的话下面应该是布局了。

最终测量与布局阶段

在 performTraversals() 方法中会调用如下代码通知 WMS 开始为布局与绘制做准备:


relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);

上述方法中会调用:


int relayoutResult = mWindowSession.relayout(
                mWindow, mSeq, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f),
                viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
                mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                mPendingStableInsets, mPendingOutsets, mPendingConfiguration, mSurface);

这个时候会通过 mWindowSession 将尺寸、位置等信息传给 WMS 完成窗口布局,并申请绘图表面,执行完了之后 mSurface 便是一个有效的 Surface 了。
然后会再次调用 performMeasure() 进行最终测量。
之后调用下面的代码开始控件布局:


performLayout(lp, desiredWindowWidth, desiredWindowHeight);

在 performLayout 方法中会调用我们熟悉的 View.layout 方法:


host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

这样完成了控件树的布局调用。

绘制阶段

绘制阶段主要是 performTraversals() 方法中调用 performDraw(), 然后调用 draw(), 对于软件绘制最终会调用 drawSoftware:


private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
    final Canvas canvas;
    try {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;
        //获取画布
        canvas = mSurface.lockCanvas(dirty);
        if (left != dirty.left || top != dirty.top || right != dirty.right
                || bottom != dirty.bottom) {
            attachInfo.mIgnoreDirtyState = true;
        }
        canvas.setDensity(mDensity);
    } catch (Surface.OutOfResourcesException e) {
        handleOutOfResourcesException(e);
        return false;
    } catch (IllegalArgumentException e) {
        mLayoutRequested = true;    // ask wm for a new surface next time.
        return false;
    }
    try {
        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }

        dirty.setEmpty();
        mIsAnimating = false;
        mView.mPrivateFlags |= View.PFLAG_DRAWN;

        try {
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;
            //绘制
            mView.draw(canvas);
            drawAccessibilityFocusedDrawableIfNeeded(canvas);
        } finally {
            if (!attachInfo.mSetIgnoreDirtyState) {
                // Only clear the flag if it was not set during the mView.draw() call
                attachInfo.mIgnoreDirtyState = false;
            }
        }
    } finally {
        try {
            //提交画布
            surface.unlockCanvasAndPost(canvas);
        } catch (IllegalArgumentException e) {
            mLayoutRequested = true;    // ask wm for a new surface next time.
            return false;
        }
    }
    return true;
}

首先会通过 Surface 拿到 Canvas:


canvas = mSurface.lockCanvas(dirty);

然后就可以通知控件树进行绘制了:


mView.draw(canvas);

最后提交画布,通知 WMS 进行软件绘制


surface.unlockCanvasAndPost(canvas);

以上就是控件绘制的所有内容,还有很多内容或者细节没有涉及,包括初次遍历的特殊处理,硬件绘制,动画渲染等。下面开始讲输入部分。

控件输入事件

Android 常见的输入包括触摸屏和键盘,当然也支持手柄,鼠标等。设备可用时 Linux 内核会在 /dev/input/ 目录下创建相应的设备节点,并且会将原始输入事件写入到对应的设备节点中,系统进程会实时读取设备节点中的信息,并将其包装成 KeyEvent、MotionEvent 派发给特定的窗口,对于 MotionEvent 最终回调 View 的 onTouchEvent 方法。这个过程由 InputManagerService、WindowManagerService 等多个系统服务组件共同完成。

先从 InputManagerService 入手,看其构造方法:


public InputManagerService(Context context) {
        this.mContext = context;
        this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());
        mUseDevInputEventForAudioJack = context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
        mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
        LocalServices.addService(InputManagerInternal.class, new LocalService());
}

需要与硬件打交道,主要逻辑肯定在 native 层,查看 nativeInit 方法实现,位于com_android_server_input_InputManagerService.cpp:


static jlong nativeInit(JNIEnv* env, jclass clazz,
        jobject serviceObj, jobject contextObj, jobject messageQueueObj) {
    sp messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
    if (messageQueue == NULL) {
        jniThrowRuntimeException(env, "MessageQueue is not initialized.");
        return 0;
    }
    NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,
            messageQueue->getLooper());
    im->incStrong(0);
    return reinterpret_cast(im);
}

创建了 native 层的 MessageQueue 与 NativeInputManager,继续查看 NativeInputManager 类的构造方法:


NativeInputManager::NativeInputManager(jobject contextObj,
        jobject serviceObj, const sp& looper) :
        mLooper(looper), mInteractive(true) {
    JNIEnv* env = jniEnv();
    mContextObj = env->NewGlobalRef(contextObj);
    mServiceObj = env->NewGlobalRef(serviceObj);
    {
        AutoMutex _l(mLock);
        mLocked.systemUiVisibility = ASYSTEM_UI_VISIBILITY_STATUS_BAR_VISIBLE;
        mLocked.pointerSpeed = 0;
        mLocked.pointerGesturesEnabled = true;
        mLocked.showTouches = false;
    }
    sp eventHub = new EventHub();
    mInputManager = new InputManager(eventHub, this, this);
}

这里出现了一个重要的对象 EventHub,EventHub 内部使用 inotify 与 epoll 机制管理着 /dev/input/ 目录下的所有节点信息,可以通过 EventHub.getEvents() 获取原始输入事件。
继续看 InputManager 的构造方法:


InputManager::InputManager(
        const sp& eventHub,
        const sp& readerPolicy,
        const sp& dispatcherPolicy) {
    mDispatcher = new InputDispatcher(dispatcherPolicy);
    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
    initialize();
}

void InputManager::initialize() {
    mReaderThread = new InputReaderThread(mReader);
    mDispatcherThread = new InputDispatcherThread(mDispatcher);
}

这里创建了四个重要的对象,InputDispatcher,InputReader,InputReaderThread,InputDispatcherThread,其中有两个线程,InputReaderThread 线程会一直去 EventHub 中读取输入事件,包装之后放入到派发队列中,InputDispatcherThread 线程将派发队列中的事件分发给对应的 Window。


res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);

绘图阶段会调用上述的方法,其中会传一个 InputChannel 对象,InputChannel 本质是一对 SocketPair,用于实现本机进程间通信。
addToDisplay 方法实际上调用的是 WindowManagerService 的 addWindow 方法,addWindow 方法中有这么一段代码:


if (outInputChannel != null && (attrs.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
    String name = win.makeInputChannelName();
    InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
    win.setInputChannel(inputChannels[0]);
    inputChannels[1].transferTo(outInputChannel);
    mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);
}

openInputChannelPair 函数在 native 层打开了一对 InputChannel 返回,其中一个 InputChannel 设置给了 WindowState,另一个交给了 outInputChannel 作为输出。并且将 WindowState 保存的 InputChannel 向 WMS 注册了。那么这样 InputDispatcher 就能将输入事件派发给注册进 WMS 的 win.mInputChannel, 然后客户端通过读取 InputChannel 就能拿到输入事件了,读取 InputChannel 的类是 WindowInputEventReceiver,在 ViewRootImpl 的 setView 中会将 mInputChannel 传进 WindowInputEventReceiver:


mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,Looper.myLooper());

如果 InputChannel 有可读事件那么就会回调 WindowInputEventReceiver 的 onInputEvent 方法。具体看 native 层的实现, 位于 android_view_InputEventReceiver.cpp 的 nativeInit 方法:


static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,
        jobject inputChannelObj, jobject messageQueueObj) {
    sp inputChannel = android_view_InputChannel_getInputChannel(env, inputChannelObj);
    sp messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
    sp receiver = new NativeInputEventReceiver(env,receiverWeak, inputChannel, messageQueue);
    status_t status = receiver->initialize();
    return reinterpret_cast(receiver.get());
}
status_t NativeInputEventReceiver::initialize() {
    setFdEvents(ALOOPER_EVENT_INPUT);
    return OK;
}
void NativeInputEventReceiver::setFdEvents(int events) {
    if (mFdEvents != events) {
        mFdEvents = events;
        int fd = mInputConsumer.getChannel()->getFd();
        if (events) {
            mMessageQueue->getLooper()->addFd(fd, 0, events, this, NULL);
        } else {
            mMessageQueue->getLooper()->removeFd(fd);
        }
    }
}

在 nativeInit 方法中构建了 NativeInputEventReceiver 对象,并且调用了它的 initialize 方法, 在 initialize 方法中将 InputChannel 的输入事件注册进了 native 层的 Looper 中,当 InputChannel 可读时便会回调 handleEvent(),最终回调到 Java 层的 onInputEvent()。
在 WindowInputEventReceiver 中 的 onInputEvent 方法会调用 enqueueInputEvent,最终调用到 deliverInputEvent:


private void deliverInputEvent(QueuedInputEvent q) {
    InputStage stage;
    if (q.shouldSendToSynthesizer()) {
        stage = mSyntheticInputStage;
    } else {
        stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
    }
    if (stage != null) {
        stage.deliver(q);
    } else {
        finishInputEvent(q);
    }
}

这里使用了责任链模式开始传递输入事件,其中一个责任链为 ViewPostImeInputStage,看它的处理方法:


protected int onProcess(QueuedInputEvent q) {
    if (q.mEvent instanceof KeyEvent) {
        return processKeyEvent(q);
    } else {
        // If delivering a new non-key event, make sure the window is
        // now allowed to start updating.
        handleDispatchWindowAnimationStopped();
        final int source = q.mEvent.getSource();
        if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
            return processPointerEvent(q);
        } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
            return processTrackballEvent(q);
        } else {
            return processGenericMotionEvent(q);
        }
    }
}

根据不同的输入类型进行分发,以 processPointerEvent 为例:


private int processPointerEvent(QueuedInputEvent q) {
    final MotionEvent event = (MotionEvent)q.mEvent;

    mAttachInfo.mUnbufferedDispatchRequested = false;
    boolean handled = mView.dispatchPointerEvent(event);
    if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
        mUnbufferedInputDispatch = true;
        if (mConsumeBatchedInputScheduled) {
            scheduleConsumeBatchedInputImmediately();
        }
    }
    return handled ? FINISH_HANDLED : FORWARD;
}

分发给了 View 的 dispatchPointerEvent:


public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

调到了我们熟悉的 dispatchTouchEvent 方法。
总结一下控件的输入事件:

  1. 原始事件由 Linux 内核写入到 /dev/input/ 目录对应的节点中
  2. native 层的 EventHub 维护了所有的设备节点,使用了 INotify,epoll 机制,通过 EventHub.getEvents 可以获取输入事件
  3. native 层的 InputManager 开启了两个线程,一个从 EventHub 读取事件,一个分发事件给焦点窗口
  4. 分发的时候会从焦点窗口获取对应的 InputChannel,并写入到 InputChannel 中
  5. 在 NativeInputEventReceiver 会将监听 InputChannel 的输入事件,并将输入事件回调给 Java 层的 InputEventReceiver
  6. ViewRootImpl 中将 InputEventReceiver 回调过来的事件分发给 View

参考:《深入理解Android 卷III》

>> 转载请注明来源:深入理解 Android 控件

评论

  • 开发者头条回复

    感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/nhrshj 欢迎点赞支持!
    欢迎订阅《pqpo》https://toutiao.io/subjects/55960

  • twiceYuan回复

    感谢分享,不过博主你这个背景简直有毒啊,看一会儿风扇就呼呼的响 😂

    • pqpo回复

      以前也有人反馈过,我电脑上用 Chrome 不会诶。如果影响阅读要考虑去掉了。

      • twiceYuan回复

        神奇了,我也是 Chrome,版本 59.0.3071.115,macOS(不过效果还是还不错的,就是不能看太久哈哈)

  • 四无小青年回复

    不要去背景!!!好喜欢这个背景能玩一年

  • gali回复

    博主这背景是怎么弄的,感觉非常有趣啊。

    • pqpo回复

      只需要加载了一个js库:jscanvas-nest.min.js

      • gali回复

        非常感谢!

  • 文文回复

    博主,我有一个问题,你选择的主题是因为实际开发中有需求,还是自己感兴趣选择的?

    • pqpo回复

      兴趣选择。这篇文章是读《深入理解Android 卷III》之后的一个总结,为了加深印象。

  • 独角兽回复

    😛 背景超酷的

回复给 pqpo 点击这里取消回复。