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


不管工作幾年的 Android 工程師,或多或少都聽說過 Window 的概念,並且隱隱約約感覺它在 Activity 與 View 之間應該發揮着某種連接的作用。但是如果需要說出這 3 者之間的關系,多數工程師不知道從何下手。

Activity 的 setContentView

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

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

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

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

記得上篇文章中分析 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,如下所示:

 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 的引用。

PhoneWindow 的 setContentView

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)以及接收觸屏事件。

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM