1. 程式人生 > >Activity展示流程原始碼閱讀

Activity展示流程原始碼閱讀

前言

前面查看了Activity啟動的整體流程,現在來看一看Activity裡定義的檢視樹是如何展示到手機螢幕上的。首先開發者通常都會在onCreate裡定義setContentView(佈局檔案ID),再執行Activity就能夠將佈局檔案中的檢視展示出來,在底層實際做做展示以及與使用者互動都是有WindowManagerService(簡稱WMS)服務來進行的,與WMS做互動的主要是Window物件,為了能夠清楚它們之間如何互動我們需要從建立Activity後呼叫attach繫結Activity和Window物件的程式碼看起。

程式碼分析

檢視Activity的attach方法,它主要負責將Activity和Window物件關聯起來,這裡需要特別注意的是WindowManager物件的初始化過程。我們知道WindowManager實際上是個介面,實現它的是WindowManagerImpl類,這個類也只是客戶端與WMS通訊的代理物件,真正的通訊過程實際上要使用WindowManagerGlobal單例物件來實現。

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) {
    attachBaseContext(context);
mFragments.attachHost(null /*parent*/); // 建立PhoneWindow物件 mWindow = new PhoneWindow(this, window, activityConfigCallback); mWindow.setWindowControllerCallback(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory
(this); // ActivityInfo中儲存了從AndroidManifest檔案中解析出來的資訊,設定軟鍵盤模式 if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { mWindow.setSoftInputMode(info.softInputMode); } if (info.uiOptions != 0) { mWindow.setUiOptions(info.uiOptions); } // 設定其他成員屬性 // 為Window物件初始化WindowManager物件 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; mWindow.setColorMode(info.colorMode); }

檢視PhoneWindow的setWindowManager方法發現它會為每個Activity建立一個WindowManagerImpl物件,但是在每個WindowManagerImpl物件裡都會使用單一的WindowManagerGlobal物件來實現和WMS通訊。為什麼要做這樣的設計呢,其實主要是因為和PhoneWindow繫結的WindowManager還需要專門記錄下它所維護的Window的父Window,這裡就多加了一箇中間層。

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    mAppToken = appToken;
    mAppName = appName;
    mHardwareAccelerated = hardwareAccelerated
            || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;
    private final Window mParentWindow;

    private IBinder mDefaultToken;

    public WindowManagerImpl(Context context) {
        this(context, null);
    }

    private WindowManagerImpl(Context context, Window parentWindow) {
        mContext = context;
        mParentWindow = parentWindow;
    }

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

    public WindowManagerImpl createPresentationWindowManager(Context displayContext) {
        return new WindowManagerImpl(displayContext, mParentWindow);
    }
    ......
}

和Activity向關聯的Window物件已經初始化完畢,這時後就會呼叫Activity.onCreate方法,開發者會呼叫setContentView來新增自己的介面佈局。檢視setContentView的實現原始碼發現實際上是呼叫了PhoneWindow的setContentView方法,這裡首先判斷mContentParent是否為空,onCreate的時候一般mContentParent是空的,就會呼叫installDecor()方法向PhoneWindow中新增DecorView根部局。

// Activity原始碼
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

// PhoneWindow原始碼
@Override
public void setContentView(int layoutResID) {

    if (mContentParent == null) {
        installDecor();
    } 

    // 判斷各種feature
}

在installDecor()方法中會首先建立DecorView根部局,它是繼承自FrameLayout的一個佈局物件,通常都只是作為Activity的根部局來使用。需要注意的是從Android7.0開始如果設定了mUseDecorContext傳遞給DecorView的context不再直接使用Activity物件了,可以看到實際上是一個DecorContext物件,如果之後直接使用View.getContext()獲得的並不是Activity物件,強轉會出現ClassCastException異常。

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    ...
}


protected DecorView generateDecor(int featureId) {
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            // 這裡需要注意使用了DecorContext
            context = new DecorContext(applicationContext, getContext().getResources());
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
}

生成完根部局DecorView之後就需要開始生成內部的包含有ActionBar和一個id為android.R.id.content的FrameLayout內容佈局物件,我們在Activity中設定的佈局資源就會被載入放到這個內容佈局中,執行完這些程式碼之後Activity的所有佈局就都已經準備完成,需要通知WMS進行展示操作。

在Activity啟動原始碼分析中我們知道在onCreate方法呼叫之後會繼續執行ActivityThread.handleResumeActivity,從原始碼中可以看到在需要展示佈局的時候會呼叫WindowManager.addView(view),通過前面的分析我們知道這個WindowManager就是WindowManageImpl型別的物件。

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    .........
    r.window = r.activity.getWindow();
    View decor = r.window.getDecorView();
    decor.setVisibility(View.INVISIBLE);
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    a.mDecor = decor;
    l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
    l.softInputMode |= forwardBit;
    if (r.mPreserveWindow) {
        a.mWindowAdded = true;
        r.mPreserveWindow = false;
        ViewRootImpl impl = decor.getViewRootImpl();
        if (impl != null) {
            impl.notifyChildRebuilt();
        }
    }
    if (a.mVisibleFromClient) {
        if (!a.mWindowAdded) {
            a.mWindowAdded = true;
            wm.addView(decor, l);
        } else {
            a.onWindowAttributesChanged(l);
        }
    }
    .......
}

在WindowManagerImpl的addView方法中會直接呼叫WindowManagerGlobal的addView方法,在這裡獲取Display物件和WindowManagerImpl儲存的父Window物件並且初始化ViewRootImpl物件,將它們的引用都儲存到WindowManagerGlobal的快取中,為以後的使用者互動動作方便查詢對應的介面。

// WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    .......
    ViewRootImpl root;
    View panelParentView = null;
    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;
    }
    .......
}

最後一步的ViewRootImpl的setView方法就是最重要的一步,我們看到最開始它會初始化一個mAttachInfo的屬性,對View比較瞭解的讀者應該對這個屬性比較熟悉,之後它還會發送一個requestLayout請求,最後在呼叫mWindowSession.addToDisplay()真正的把Window裡的DecorView新增到WMS當中,後面的程式碼會對addToDisplay操作的結果進行判定,可見這個跨程序通訊是同步執行的。瞭解了整個過程之後我們在來檢視requestLayout裡的詳細實現邏輯。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
    ......
    mAttachInfo.mRootView = view;
    mAttachInfo.mScalingRequired = mTranslator != null;
    mAttachInfo.mApplicationScale =
            mTranslator == null ? 1.0f : mTranslator.applicationScale;
    if (panelParentView != null) {
        mAttachInfo.mPanelParentWindowToken
                = panelParentView.getApplicationWindowToken();
    }
    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();

    #########################################################################################


    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) {
        mAttachInfo.mRootView = null;
        mAdded = false;
        mFallbackEventHandler.setView(null);
        unscheduleTraversals();
        setAccessibilityFocus(null, null);
        switch (res) {
            case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
            case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                throw new WindowManager.BadTokenException(
                        "Unable to add window -- token " + attrs.token
                        + " is not valid; is your activity running?");
            case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                throw new WindowManager.BadTokenException(
                        "Unable to add window -- token " + attrs.token
                        + " is not for an application");
    }
    ......
}

在requestLayout中首先會判定當前的執行緒是否是主執行緒,如果不是會直接丟擲CalledFromWrongThreadException異常,如果在子執行緒中改變View的展示屬性就會丟擲這個異常。後面呼叫scheduleTraversals方法並且使用mChoreographer提交了一個TraversalRunnable物件。其中mChoreographer物件主要負責根據VSync訊號或者系統幀率自動重新整理螢幕功能,這裡使用的是postCallback方法也就是說會立即執行doTraversal不會放到佇列中延遲執行。

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

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

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

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

doTraversal方法中會直接呼叫performTraversals方法,performTraversals方法在第一次被呼叫使會執行dispatchAttachedToWindow也就是將mAttachInfo設定到DecorView裡面的所有子View中,後面executeActions就是執行在View.postXXX方法提交的回撥。比如想要獲取某個View的寬高,直接在onCreate方法中是無法獲取的因為這時候還沒有測量,可以使用view.postXXX回撥裡能夠拿到寬高,需要注意的是這些回撥是放到Handler裡執行的,雖然它們在performMeasure之前其實真正的執行是performTraversals方法結束之後。後面的performMeasure是從DecorView根View開始遞迴測量檢視樹中的所有View的寬高,performLayout則是從DecorView開始遞迴的將View放到指定的位置,最後面的performDraw方法會呼叫View的draw方法將所有的View內容畫到螢幕上,這樣整個Activity就展示出來了。

private void performTraversals() {
    // mView也就是setView傳進來的DecorView
    final View host = mView;
    if (mFirst) {
        // 如果是第一次執行performTraversals
        host.dispatchAttachedToWindow(mAttachInfo, 0);
    }

    .....
    getRunQueue().executeActions(mAttachInfo.mHandler);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    performDraw();
    .....
}

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

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    ....
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());   
    ....
}

前面還提到了WindowSession和WMS程序間通訊,這個其實比較底層了,對於Android應用開發者而言瞭解到performTraversals方法的邏輯就足夠了。

總結

Activity內部包含了PhoneWindow物件,PhoneWindow物件管理者DecorView物件,DecorView內部包含了使用者自定義的所有展示佈局;WindowManagerGlobal管理這整個應用中所有介面的展示,它通過ViewRootImpl實現和系統WMS服務的互動功能,ViewRootImpl只負責一個Window的互動操作;ViewRootImpl通過呼叫requestLayout方法實現DecorView整棵檢視樹的測量、佈局和繪製工作,使用WindowSession物件將DecorView新增到WMS服務,WMS服務負責將DecorView的內容展示到手機螢幕上。