原文鏈接:
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。下面的圖,說明了這一點。
一個好漢三個幫,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。
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 機制在 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 是如何實現的。再會 :)
文檔信息
- 版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證)
- 發表日期:2016年10月10日
- 社交媒體:weibo.com/woaitqs
- Feed訂閱:www.woaitqs.cc/feed.xml