底層剖析 Window 、Activity、 View 三者關係

無論工做幾年的 Android 工程師,或多或少都據說過 Window 的概念,而且隱隱約約感受它在 Activity 與 View 之間應該發揮着某種鏈接的做用。可是若是須要說出這 3 者之間的關係,多數工程師不知道從何下手。java

Activity 的 setContentViewapp

Activity 是 Android 開發人員使用最頻繁的 API 之一,最初在接觸 Android 開發時,我始終認爲它就是負責將 layout 佈局中的控件渲染繪製出來的。緣由很簡單,每當咱們想顯示一個新的界面時,都是經過 start 一個新的 Activity 方式;對於想顯示的內容或者佈局,也只須要在 Activity 中添加一行 setContentView 便可,剩下的 Activity 都自動幫咱們搞定。可是咱們歷來沒有去建立一個 Window 來綁定 UI 或者 View 元素。ide

直到我點開 setContentView 源碼的那一刻:佈局

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
public Window getWindow() {
        return mWindow;
    }

顯然 Activity 幾乎什麼都沒作,將操做直接交給了一個 Window 來處理。getWindow 返回的是 Activity 中的全局變量 mWindow,它是 Window 窗口類型。那麼它是何時賦值的呢?post

記得上篇文章中分析 startActivity 的過程,最終代碼會調用到 ActivityThread 中的 performLaunchActivity 方法,經過反射建立 Activity 對象,並執行其 attach 方法。Window 就是在這個方法中被建立,詳細代碼以下:學習

@UnsupportedAppUsage
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        ...

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

在 Activity 的 attach 方法中將 mWindow 賦值給一個 PhoneWindow 對象,實際上整個 Android 系統中 Window 只有一個實現類,就是 PhoneWindow。優化

接下來調用 setWindowManager 方法,將系統 WindowManager 傳給 PhoneWindow,以下所示:ui

 public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated;
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
 mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);     }
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

最終,在 PhoneWindow 中持有了一個 WindowManagerImpl 的引用。this

PhoneWindow 的 setContentViewspa

Activity 將 setContentView 的操做交給了 PhoneWindow,看下PhoneWindow的setContentView方法實現過程:

@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

解釋說明:

  • 標紅的1 處調用若是 mContentParent 爲 null,則調用 installDecor 初始化 DecorView 和 mContentParent。
  • 標紅的2處將咱們調用 setContentView 傳入的佈局添加到 mContentParent 中。

 

能夠看出在 PhoneWindow 中默認有一個 DecorView(其實是一個 FrameLayout),在 DecorView 中默認自帶一個 mContentParent(其實是一個 ViewGroup)。咱們本身實現的佈局是被添加到 mContentParent 中的,所以通過 setContentView 以後,PhoneWindow 內部的 View 關係以下所示:

 

 

 

目前爲止 PhoneWindow 中只是建立出了一個 DecorView,並在 DecorView 中填充了咱們在 Activity 中傳入的 layoutId 佈局,但是 DecorView 尚未跟 Activity 創建任何聯繫,也沒有被繪製到界面上顯示。那 DecorView 是什麼時候被繪製到屏幕上的呢?

剛接觸 Android,學習生命週期時,常常會看到相關文檔介紹 Activity 執行到 onCreate 時並不可見,只有執行完 onResume 以後 Activity 中的內容纔是屏幕可見狀態。形成這種現象的緣由就是,onCreate 階段只是初始化了 Activity 須要顯示的內容,而在 onResume 階段纔會將 PhoneWindow 中的 DecorView 真正的繪製到屏幕上。

在 ActivityThread 的 handleResumeActivity 中,會調用 WindowManager 的 addView 方法將 DecorView 添加到 WMS(WindowManagerService) 上,以下所示:

@Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        ...
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }

WindowManger 的 addView 結果有兩個:

  • DecorView 被渲染繪製到屏幕上顯示;
  • DecorView 能夠接收屏幕觸摸事件。

WindowManager 的 addView

PhoneWindow 只是負責處理一些應用窗口通用的邏輯(設置標題欄,導航欄等)。可是真正完成把一個 View 做爲窗口添加到 WMS 的過程是由 WindowManager 來完成的。

WindowManager 是接口類型,它真正的實現者是 WindowManagerImpl 類,看一下它的 addView 方法以下:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

  ...

 root = new ViewRootImpl(view.getContext(), display); 
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
 root.setView(view, wparams, panelParentView);             } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }

WindowManagerImpl 也是一個空殼,它調用了 WindowManagerGlobal 的 addView 方法。

WindowMangerGlobal 是一個單例,每個進程中只有一個實例對象。如上圖紅框中所示,在其 addView 方法中,建立了一個最關鍵的 ViewRootImpl 對象,而後經過 ViewRootImpl 的 setView 方法將 view 添加到 WMS 中。

ViewRootImpl 的 setView

/**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

                mAttachInfo.mDisplayState = mDisplay.getState();
                mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);

                mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
                mFallbackEventHandler.setView(view);
                mWindowAttributes.copyFrom(attrs);
                if (mWindowAttributes.packageName == null) {
                    mWindowAttributes.packageName = mBasePackageName;
                }
                attrs = mWindowAttributes;
                setTag();

                ...
                mAdded = true;
                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();
                }
                mForceDecorViewVisibility = (mWindowAttributes.privateFlags
                        & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel, mTempInsets);
                    setFrame(mTmpFrame);
                } 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();
                    }
                }

                ...
            }
        }
    }

解釋說明:

  • 標紅的 1 處的 requestLayout 是刷新佈局的操做,調用此方法後 ViewRootImpl 所關聯的 View 也執行 measure - layout - draw 操做,確保在 View 被添加到 Window 上顯示到屏幕以前,已經完成測量和繪製操做。
  • 標紅的2 處調用 mWindowSession 的 addToDisplay 方法將 View 添加到 WMS 中。

WindowSession 是 WindowManagerGlobal 中的單例對象,初始化代碼以下:

 

@UnsupportedAppUsage
    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    // Emulate the legacy behavior.  The global instance of InputMethodManager
                    // was instantiated here.
                    // TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
                    InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) { ValueAnimator.setDurationScale(scale); } });
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
    }

sWindowSession 其實是 IWindowSession 類型,是一個 Binder 類型,真正的實現類是 System 進程中的 Session。上面代碼中標紅的就是用 AIDL 獲取 System 進程中 Session 的對象。其 addToDisplay 方法以下:

 

 

 

 圖中的 mService 就是 WMS。至此,Window 已經成功的被傳遞給了 WMS。剩下的工做就所有轉移到系統進程中的 WMS 來完成最終的添加操做。

 

再看 Activity

以前提到 addView 成功有一個標誌就是可以接收觸屏事件,經過對 setContentView 流程的分析,能夠看出添加 View 的操做實質上是 PhoneWindow 在全盤操做,背後負責人是 WMS,反之 Activity 自始至終沒什麼參與感。可是咱們也知道當觸屏事件發生以後,Touch 事件首先是被傳入到 Activity,而後才被下發到佈局中的 ViewGroup 或者 View。那麼 Touch 事件是如何傳遞到 Activity 上的呢?

ViewRootImpl 中的 setView 方法中,除了調用 IWindowSession 執行跨進程添加 View 以外,還有一項重要的操做就是設置輸入事件的處理:

// Set up the input pipeline.
                CharSequence counterSuffix = attrs.getTitle();
                mSyntheticInputStage = new SyntheticInputStage();
                InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
                InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
                        "aq:native-post-ime:" + counterSuffix);
                InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
                InputStage imeStage = new ImeInputStage(earlyPostImeStage,
                        "aq:ime:" + counterSuffix);
                InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
                InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
                        "aq:native-pre-ime:" + counterSuffix);

如上所示,設置了一系列的輸入通道。一個觸屏事件的發生是由屏幕發起,而後通過驅動層一系列的優化計算經過 Socket 跨進程通知 Android Framework 層(實際上就是 WMS),最終屏幕的觸摸事件會被髮送到上圖中的輸入管道中。

這些輸入管道其實是一個鏈表結構,當某一個屏幕觸摸事件到達其中的 ViewPostImeInputState 時,會通過 onProcess 來處理,以下所示:

final class ViewPostImeInputStage extends InputStage {
        public ViewPostImeInputStage(InputStage next) {
            super(next);
        }

        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                return processKeyEvent(q);
            } else {
                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);
                }
            }
        }
 private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;

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

能夠看到在 onProcess 中最終調用了一個 mView的dispatchPointerEvent 方法,mView 實際上就是 PhoneWindow 中的 DecorView,而 dispatchPointerEvent 是被 View.java 實現的,以下View中dispatchPointerEvent方法所示:

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

DecorView中的dispatchTouchEvent方法代碼:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

最終調用了 PhoneWindow 中 Callback的dispatchTouchEvent 方法,那這個 Callback 是否是 Activity 呢?

在啓動 Activity 階段,建立 Activity 對象並調用 attach 方法時,有以下一段代碼:

@UnsupportedAppUsage
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
 mWindow.setCallback(this);         mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);

果真將 Activity 自身傳遞給了 PhoneWindow,再接着看 Activity的dispatchTouchEvent 方法:

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

PhoneWindow中

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

Touch 事件在 Activity 中只是繞了一圈最後仍是回到了 PhoneWindow 中的 DecorView 來處理。剩下的就是從 DecorView 開始將事件層層傳遞給內部的子 View 中了。

 

總結

文中主要經過 setContentView 的流程,分析了 Activity、Window、View 之間的關係。整個過程 Activity 表面上參與度比較低,大部分 View 的添加操做都被封裝到 Window 中實現。而 Activity 就至關於 Android 提供給開發人員的一個管理類,經過它可以更簡單的實現 Window 和 View 的操做邏輯。

最後再簡單列一下整個流程須要注意的點:

1》一個 Activity 中有一個 window,也就是 PhoneWindow 對象,在 PhoneWindow 中有一個 DecorView,在 setContentView 中會將 layout 填充到此 DecorView 中。
2》一個應用進程中只有一個 WindowManagerGlobal 對象,由於在 ViewRootImpl 中它是 static 靜態類型。
3》每個 PhoneWindow 對應一個 ViewRootImple 對象。
4》WindowMangerGlobal 經過調用 ViewRootImpl 的 setView 方法,完成 window 的添加過程。
5》ViewRootImpl 的 setView 方法中主要完成兩件事情:View 渲染(requestLayout)以及接收觸屏事件。

 

————來自拉勾教育筆記

相關文章
相關標籤/搜索