Android View 全解析(一) -- 窗口管理系統

原文鏈接:
http://www.woaitqs.cc/android/2016/10/10/android-view-theory-1.html

一直在寫 Android Framework 層的東西,雖然很重要,但一直寫這方面的內容,還是有些不夠接地氣。思前想後一番後,打算寫寫關於 View 這個在 Android 中平常得不能更平常的東西。由淺入深,仔細梳理,幫助我,也希望能幫助你,更好地再次認識 View。

第一篇文章主要講 Android 的窗口管理系統,依託於這套系統,我們才能將 View 顯示到屏幕上。瞭解這套系統,有助於更好地理解 Android View 的來龍去脈。但這個系統複雜,涉及到的方面衆多,在這裏也只是簡述,還望見諒。


窗口管理系統大家族

這個小節,主要說一下家族裏面的成員,分別是 view,window,windowmanager,viewRoot。小節裏面主要介紹它們是什麼,以及互相之間的關係是什麼。

在談及 View 之前,必須得說說 Window 這個東西,通常我們認爲顯示在界面上的是 View,這麼說本身沒有什麼問題,但更準確的說法是 WindowManager 通過 ViewRoot 將 View 和 Window 協同整合在一起,最終將 View 展示在 Window 上面。正如 Window 這個名字,就是窗口的意思,我們所見的所有東西都要展示在 Window 上,熟知的 Dialog、Activity 以及 Toast 都是展示在 Window 上面的。

Window 本身是一個抽象類,提供了對標準 UI 行爲的一些支持,例如背景、標題欄和按鍵等等。我們使用的是它的子類,也是唯一的實現 PhoneWindow。

Window的顯示有多種類型,具體可以在這裏看到 https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html。普通開發的應用,就是類型爲 TYPE_APPLICATION 的 Window。最近一些工具應用開始使用懸浮窗,特別是一些手機清理軟件,懸浮窗通常採用的是 TYPE_SYSTEM_ALERT、TYPE_PHONE, 對於 API 在 19 以上的系統,使用的是 TYPE_TOAST。

成功的男人背後都有一個偉大的女人,一個成功的 Window 背後就有一個偉大的 WindowMananger。WindowManager 本身是一個接口,提供了與 Window 交互的基礎功能,分別是添加、更新和刪除 View 的接口。

public interface ViewManager {
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

WindowManager 的實現沿用了 C/S 結構,WindowManager 只作爲一個代理,實際工作的是 WindowManagerService。WindowManangerService 以 Session 的形式來管理各個 Application 的窗口,系統啓動了多少個含有 View 的應用,就有多少個對應的 Session。下面的圖,說明了這一點。

圖片來自 www.programgo.com

一個好漢三個幫,View 系統也不例外,WindowManager 與 View 之間不直接進行交互,而是依託於一箇中間商,叫做 ViewRoot。ViewRoot 本身是一個 Handler,通過 ViewRoot 實現了兩者間的消息傳遞。ViewRoot 將通知 View 進行相應的界面繪製,然後調用 WindowManager 提供的接口,將 View 添加或更新到 Window 上面。

添加 DecorView 到 PhoneWindow

我們以 Activity 的 setContentView 入手,看看窗口管理系統是如何介入這個過程的。

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

這裏調用了 getWindow 方法,需要解釋的是對於每個 Activity 而言都有一個對應的窗口,就是前文提及的 PhoneWindow,這個 PhoneWindow 有一個 View ,叫做 DecorView,這也是界面佈局的根節點。隨便找了一個 App 的界面,大家可以注意箭頭標示的地方,那裏就是 DecorView。

decor view

setContentView 的實現在其子類 PhoneWindow 中,上面的圖也可以看到。

@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();
    }
}

重點在於 installDecor 方法中,這裏是給 Window 添加根 View,也就是 DecorView。

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);
        // ...
    }

    // ...

}

在這個方法中完成了 Transaction 動畫的設置,標題欄的配置,decor 背景的設置等等初始化工作。代碼相對比較繁瑣,有興趣的同學,可以看看源碼中 PhoneWindow 的實現。

在這之後,View 就可以顯示了嗎?大家可能也不會覺得是這麼簡單的,畢竟我們熟知的 onMeasure、onLayout 和 onDraw 都還沒有調用呢?看起來,我們需要一個時機來觸發 View 的操作。在下一小節,就來說一下什麼時候觸發這一過程。

View 什麼時候開始繪製

Android 系統在 4.0 後爲更好的用戶體驗,執行了一個「黃油計劃」,而其中最重要的部分就是垂直同步機制(VSYNC)。讓我們首先在大體上理解一下 VSYNC。

VSync stands for Vertical Synchronization. The basic idea is that synchronizes your FPS with your monitor’s refresh rate. The purpose is to eliminate something called “tearing”. I will describe all these things here. Every CRT monitor has a refresh rate.

這是從一篇論文裏面引用過來的,VSYNC 就是一種同步機制,以某種固定的頻率進行同步,當其他組件收到這個同步信號時,就執行相應的操作。設想一下,如果沒有這個同步機制,各個模塊又怎能知道在哪個時候去執行自己的工作了? 這裏可以初步地將 VSYNC 當做鬧鐘,每間隔固定時間,就響一次,其他組件聽到鬧鈴後,就開始幹活了。這個間隔的時間,與屏幕刷新頻率有關,例如大多數 Android 設備的刷新頻率是 60 FPS(Frame per second),一秒鐘刷新60次,因而間隔時間就是 1000 / 60 = 16.667 ms。這個時間,大家是不是很熟悉了?看過太多性能優化的文章,都說每一幀的繪製時間不要超過 16 ms,其背後的原因就是這個。繪製每一幀對應的 View,這個步驟發生在 UI 線程上,所以也不要在 UI 線程上進行耗時的操作,否則就可能在 16 ms內,無法完成界面更新操作了。

VSYNC

圖中中間部分,顯示了試用 VSYNC 技術後的顯示效果,可以看到整體上,效果要優於其他方案。

爲了將 VSYNC 機制在 Framework 落地下來,在 View 層引入了 Choreographer。這個類的主要責任就是協調動畫、輸入和繪製,使得用戶體驗上達到一致的效果。通常情況下,我們不用去理會這個 Choreographer,Android Framework 通過更高層級的抽象,幫我們完成了這一步驟,他們分別實現在 Animation,onDraw 等代碼中。

下面大體上,看看 Choreographer 的實現。

private static final ThreadLocal<Choreographer> sThreadInstance =
        new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
        Looper looper = Looper.myLooper();
        if (looper == null) {
            throw new IllegalStateException("The current thread must have a looper!");
        }
        return new Choreographer(looper);
    }
};

這裏採用了 ThreadLocal 這個關鍵字,使用這個關鍵字後,能夠保證對於單個線程,只能獲取到同一實例。而 Choreographer 在構建的時候,還要求這個線程必須有 Looper,Choreographer 需要利用 Looper 來進行相應的消息通知。再看看構造函數的實現。

private Choreographer(Looper looper) {
    mLooper = looper;
    mHandler = new FrameHandler(looper);
    mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
    mLastFrameTimeNanos = Long.MIN_VALUE;

    mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
}

新建了一個 Handler 用來做消息通信,其後建立了一個 CallbackQueue,這裏的 CallbackQueue 主要有四種形式,當收到 VSYNC 信號後,依次執行其中的 Callback。

try {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");

    mFrameInfo.markInputHandlingStart();
    doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

    mFrameInfo.markAnimationsStart();
    doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

    mFrameInfo.markPerformTraversalsStart();
    doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

    doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

這裏的 CALLBACK_TRAVERSAL 就是與 View 繪製相關的部分。在 ViewRootImpl 代碼中,也可以證實到這一點。

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

這裏的 mTraversalRunnable 實現非常簡單。

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

而從 doTraversal 開始,View 就開始真正進行繪製了。在 Choreographer 的英明領導下,View 開始準備開工幹活了。

這裏和前面的知識進行下串聯,再進行下總結。

當 Choreographer 接收到 VSYNC 信號後,ViewRootImpl 調用 scheduleTraversals 方法,通知 View 進行相應的渲染,其後 ViewRootImpl 將 View 添加或更新到 Window 上去。

在 doTraversal 方法中,會調用到 performTraversals 方法,這是一個巨長的方法,在 API-23 版本中,代碼行數達到了800行。我們絕大多數的類,都沒有達到這個數量級 :)。在這個方法裏,就會執行到我們熟悉的 View 三部曲,Measure、Layout 和 Draw。

在後續的文章中,再來詳細說明 View 中的 Measure、Layout 和 Draw 是如何實現的。再會 :)


文檔信息


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