深刻理解 Android 控件

pqpo博客地址:深刻理解 Android 控件java

概述

本篇文章主要經過源碼講述 Android 控件系統,包括輸入事件是如何產生的, View 是如何繪製的,輸入事件是如何傳遞給 View 的,Window token 與 type 之間的聯繫等。整個系統比較複雜,每一個部分只能點到爲止,有興趣能夠繼續深刻,主要是讓讀者對 Android 控件系統有一個大致的認識。android

例子

下面是建立 Window 並顯示 View 最簡單的一個例子:canvas

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。session

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。app

WMS 中會有一個 Map 保存全部的 WindowToken,其中 key 爲 WindowManager.LayoutParams 中設置的 token,能夠是任意一個 IBinder 對象,value 爲 WindowToken,WindowToken 是由 WMS 建立的,同時會保存應用傳過來的 token 對象。ide

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操做方法,分別是新增,修改和移除:oop

windowManager.addView(imageView, layoutParams);
windowManager.updateViewLayout(imageView, layoutParams);
windowManager.removeView(imageView)複製代碼

Android 控件系統至關複雜,可是 Android 工程師們爲咱們高度封裝了 WindowManager,簡潔到只有三個方法。佈局

從 WindowManager 開始

WindowManager 是一個繼承於 ViewManager 的接口,實際類型是WindowManagerImpl,事實上 ViewGroup 也繼承於 ViewManager 接口,可見 WindowManagerImpl 的功能與 ViewGroup 相似,提供了一個顯示 View 的容器。
能夠經過下面的方法拿到 WindowManager 的實例:post

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》

相關文章
相關標籤/搜索