Android 應用界面顯示流程

Android最重要的兩個模塊(個人認爲),線程和UI。

線程我之前寫了一篇博客了,感覺還算滿意。AsyncTask源碼解析 從AsyncTask講到線程池

至於UI,趁最近有空,必須得好好整理一下腦子裏各種零碎的知識點+再學習學習,整理出幾篇博客才行。


初探Window

相信大部分學習Android第一個學到的都是Activity,如果沒有研究一下,很容易會理解成Activity就是一張白紙,然後我們在上面畫上各種View。

但其實並不是,因爲Android同一時間並不是只繪製一個應用。比如狀態欄就不是某個我們開發的應用繪製的。比如我們處於A應用時,能看到B應用的Toast。比如我們隨時能看到QQ、微信的消息懸浮窗。比如在Android早些時代,那些各種衛士吹噓的提速手機用的懸浮球。這些例子都說明,Android需要同一時間繪製多個進程所要顯示的UI。

很容易猜出,Android應該有個統一調度,然後繪製各個應用UI的大管家。事實上也的確如此,就是我們的SystemServer進程裏面的WindowManagerService這個服務了。如同ActivityManagerService用於管理所有應用的Activity,PackageManagerService用於管理所有應用的信息查詢一樣。手機屏幕的某一時刻可能同時有多個Window疊放並存着,此時就需要WindowManagerService(簡稱WMS)管理協調他們。


Window的定義和分類

所以Window是個什麼東西?

網上一直有個很形象的比喻。Activity就像工匠,Window就像是窗戶,View就像是窗花,LayoutInflater像剪刀,Xml配置像窗花圖紙。

Window就是用來承載和管理View的。可能剛開始Android設計人員並沒有區分出Activity和Window兩者,把他們看成一個整體。但是爲了獨立生命週期,任務棧等(Activity作用) 和 View繪製(Window作用)這些功能降低耦合,把這個整個劃分爲現在這樣了。每個Activity都會持有一個Window,每個Window會持有一個DecorView。


根據《Android開發藝術探索》,Window 有三種類型,分別是應用 Window、子 Window 和系統 Window。應用類 Window 對應一個 Acitivity,子 Window 不能單獨存在,需要依附在特定的父 Window 中,比如常見的一些 Dialog 就是一個子 Window。系統 Window是需要聲明權限才能創建的 Window,比如 Toast 和系統狀態欄都是系統 Window。

Window 是分層的,每個 Window 都有對應的 z-ordered,層級大的會覆蓋在層級小的 Window 上面,這和 HTML 中的 z-index 概念是完全一致的。在三種 Window 中,應用 Window 層級範圍是 1~99,子 Window 層級範圍是 1000~1999,系統 Window 層級範圍是 2000~2999。


從setContentView開發分析執行的流程

以android5.1.1的源碼來分析,我們讓Activity顯示出View的方法是setContentView,它有3個重載方法。

    private Window mWindow;

    public void More ...setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }

    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }

    public Window getWindow() {
        return mWindow;
    }

裏面的工作都是調用Window的setContentView,可以看出Activity的視圖其實是附屬在Window上的。這個Window的實際對象是一個PhoneWindow,是唯一一個繼承Window的子類。這個Window是在Activity的attach方法裏創建了,先略過等下再說,我們先找到PhoneWindow.java看看裏面的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);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

    ...

    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            ...
    }

     ...

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }

    protected ViewGroup generateLayout(DecorView decor) {
    	...
    	//layoutResource會根據是否dialog情況,title情況,actionbar情況,去取不同的xml資源。
    	View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    	...
    	return contentParent;
    }

PhoneWindow.setContentView()裏先執行了installDecor(),這個方法也是第一時間執行generateDecor(),其實就是new出來一個DecorView。DecorView是一個根佈局,是一個FrameLayout。DecorView如果初始化完成,會是這樣的結構。當然這只是其中的一種,事實上會根據主題,Title情況而有不同的結構。


這是精簡了一些不切題的控件後的圖,完整的應該是這樣的。有些同學喜歡在DecorView外圍再畫一層Window,Window外圍再畫一層Activity。的確結構是Activity持有一個Window實例,Window持有一個DecorView實例。但個人感覺並不應該這麼畫出來,畢竟他們不是ViewGroup和View這樣的控件樹層級結構,會讓人感覺有歧義。


分析下代碼,generateLayout方法裏,layoutResource會根據是否dialog情況,title情況,actionbar情況,去取不同的xml資源。inflate出來後DecorView會調用addView方法把它添加進自身裏。可以在android源碼的res/layout目錄找到這些文件。

比如在這個界面上,就是上圖DecorView的第一個子View,一個LinearLayout佈局。所以成員變量mContentRoot就是這個LinearLayout了。

而看到ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT)這句代碼,就是從佈局中找到id爲android:id/content的這個佈局,比如在這裏就是ContentFrameLayout了,最終賦值給成員變量mContentParent。


再回到PhoneWindow的setContentView(int)的方法裏,裏面會執行一個我們比較熟悉的方法。mLayoutInflater.inflate(layoutResID, mContentParent),所以inflater會創建出我們傳進去的layout資源(比如是一個activity_main.xml佈局文件),然後添加mContentParent裏面。到這裏就完成Activity的佈局文件附在DecorView上面。


到現在爲止,我們DecorView已經準備好了,但是這時候DecorView還不能起作用顯示出來。因爲WindowManager還沒有接管這個DecorView。這方面代碼在ActivityThread.java裏面。



根據我之前一篇博客《android Activity啓動過程 簡析》,Activity的顯示會走到依次走到ActivityThread.handleLaunchActivity -> ActivityThread.handleResumeActivity裏。

來看下里面的ActivityThread.handleResumeActivit做了什麼處理

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {

            ...
        ActivityClientRecord r = performResumeActivity(token, clearHide);

        if (r != null) {
        	...
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                ...
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }
            ...
    }

執行到Activity.makeVisible()方法,如下

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

makeVisible方法裏,看到一句wm.addView(mDecor, ...)。並把Decor設置成visible。

先從名字看出wm的引用類型是個WindowManager,但WindowManager是個接口,繼承自ViewManager接口,接口當然創建不出對象。沿着getWindowManager()方法跟蹤進去,發現成員變量mWindowManager在attach方法裏被賦值。

    public WindowManager getWindowManager() {
        return mWindowManager;
    }

    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) {
    	...
        mWindow = PolicyManager.makeNewWindow(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        ...
        mUiThread = Thread.currentThread();
        ...
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        ...
        mWindowManager = mWindow.getWindowManager();
        ...
    }
在attach方法裏,通過PolicyManager類new出了一個PhoneWindow。並想通過Window成員變量持有WindowManager。
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);
}

看setWindowManager方法,原來先是通過獲取SystemService的裏面的WindowManager服務,然後用createLocalWindowManager方法創建出一個本地的WindowManager的意思?。看下里面的這個create方法

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

直接new出了一個新的WindowManagerImpl實例。


然後說到調用了WindowManagerImpl.addView(mDecor, ...),進去看看addView方法

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}

這裏面調用了一個代理類mGlobal的方法,進去看WindowManagerGlobal.addView()方法。提前爆料一下WindowManagerImpl的很多方法都會交給WindowManagerGlobal實現。

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ...
    ViewRootImpl root;
    ...

    synchronized (mLock) {
      	...
        root = new ViewRootImpl(view.getContext(), display);
        ...
    }

    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        ...
    }
}

addView方法裏,會創建一個ViewRootImpl的實例。ViewRootImpl並不是View,不像它的名字一樣,它並不是Activity的根佈局,Decor纔是根佈局。PhoneWindow是使用這個ViewRootImpl來管理Decor的,可以理解爲PhoneWindow使用了ViewRootImpl來建造了Decor。PhoneWindow和ViewRootImpl是1對1的關係。

然後調用setView方法,view依然是DecorView。

final W mWindow;

public ViewRootImpl(Context context, Display display) {
	...
	mWindow = new W(this);
	...
}

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
	...
	// 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();
	...
	res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
        getHostVisibility(), mDisplay.getDisplayId(),
        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mInputChannel);
	...
}

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

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

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

void doTraversal() {
    ...
    performTraversals();
    ...
}

private void performTraversals() {
	...
	performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	...
	performLayout(lp, desiredWindowWidth, desiredWindowHeight);
	...
	performDraw();
	...
}

static class W extends IWindow.Stub {
	private final WeakReference<ViewRootImpl> mViewAncestor;
    private final IWindowSession mWindowSession;

    W(ViewRootImpl viewAncestor) {
        mViewAncestor = new WeakReference<ViewRootImpl>(viewAncestor);
        mWindowSession = viewAncestor.mWindowSession;
    }
	...
}

看到setView方法裏,這句mWindowSession.addToDisplay方法。首先mWindowSession是一個Session實例。在ViewRootImpl持有的IWindowSession引用,由WindowManagerGlobal.java執行具體的創建邏輯,實際調用WindowManagerService的openSession方法,new出一個返回。

它是幹嘛的呢?像我以前寫B/S後臺時,session表示一個用戶對於一個網站的身份標識。這裏WindowSession就是應用程序想與WMS通信的一個標識。

然後mWindowSession的引用是父類IWindowSession類型持有子類Session對象,IWindowSession是Android跨進程通信那套的aidl文件,所以在Session的addToDisplay方法裏。

    final WindowManagerService mService;

    public Session(WindowManagerService service, IWindowSessionCallback callback,
             IInputMethodClient client, IInputContext inputContext) {
        mService = service;
        ...
    }

    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outInputChannel);
    }

它將調用WindowManagerService的addWindow,把最後這個IWindow交給WMS管理。IWindow和ViewRootImpl是雙向持有的關係。所以WMS拿到IWindow,就能拿到ViewRootImpl,就能操作到DecorView。



setView的另一個重要方法是requestLayout,如下是方法調用流程requestLayout() -> scheduleTraversals() -> doTraversal() -> performTraversals()。

performTraversals這個方法厲害了,總共有700多行代碼(Android5.1.1)。我們知道一個Activity的根佈局就是DecorView,就是在這個方法裏,開始繪製這個DecorView的第一步的。熟悉自定義控件,我們都知道自定義需要我們去重寫onMeasure、onLayout和onDraw方法,對應的performTraversals也有3個方法performMeasure、performLayout和performDraw。由於對View的操作是遞歸的,所以會不斷的去進入子View進行測量、佈局和繪製,最終完成整個Activity界面的繪製。


總結一下

1.Activity裏面setContentView,實際上是利用PhoneWindow類創建DecorView,然後inflater出xml佈局,放進DecorView內。此時View層級結構已經準備好,但還沒跑measure、layout和draw三件套,也沒有被大佬WMS接管顯示出來。

2.想被WMS接管,需要操作IPC那套流程,所以需要使用WindowManager進行操作,它會通過ViewRootImpl,使用IWindowSession和IWindow兩個IPC接口(應用程序訪問WMS使用IWindowSession,WMS訪問應用程序使用IWindow),最終讓WMS接管。

3.界面顯示出來是,會經過一系列流程條用到ViewRootImpl.performTraversals方法,裏面會從DecorView開始,遞歸的執行measure、layout和draw三件套方法,最終確定大小,繪製界面。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章