探究 Android View 繪製流程,Activity 的 View 如何展示到螢幕
基於 Android API 26 Platform 原始碼
寫作背景
在上一篇探究Android View 繪製流程,Xml 檔案到 View 物件的轉換過程我們瞭解了setContentView(resId)
如何把 xml 檔案轉換成 Java 中的 View 物件。本篇文章在此基礎上繼續探究,View 是如何展示到 Activity 上的。
很多 Android 開發者都知道一個事情
當 Activity 執行 onResume() 方法後,代表 Activity 顯示到前臺
這句話很短,但是背後隱藏了多少方法的呼叫呢?下面我們將一層一層的剝開原始碼尋找真相。
先從 setContentView(resId) 入手
先說明一下,從 Android 的 Launcher 上點選應用的 Icon 的啟動過程比較複雜,本人仍在學習。如果想了解如何啟動一個 Activity 的過程可以參考Android Launcher 啟動 Activity 的工作過程,這裡我們只從關注 Activity 中的 View 顯示出來。所以直接從 Activity 的一些方法入手。
在 Activity 的 onCreate(savedInstanceState)
中呼叫 setContentView(resId)
,而setContentView(resId)
則會呼叫 PhoneWindow.setContentView(layoutResID)
原始碼並不是太長
@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();
}
}
這裡忽略轉場動畫
和一些回撥相關的邏輯程式碼後如下
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
mContentParent.requestApplyInsets();
其中 mContentParent 是一個 ViewGroup
引用
private ViewGroup mContentParent;
這樣開程式碼比較簡單明瞭
1. 判斷 mContentParent 是否為空,如果為空執行 installDecor()
2. 如果 mContentParent 不為空,清除 mContentParent 的所有子 View
3. 把傳入的佈局檔案轉換為 View 物件新增到 mContentParent
分析 installDecor()
然後我們再看下 installDecor()
,因為原始碼比較長,我們分成幾個部分解讀
第一部分
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
這幾行程式碼最重要的是呼叫了方法 generateDecor()
其實就是建立一個 DecorView
。這裡是不是能想到探究Android View 繪製流程,Canvas 的由來中最後的那張圖,我們做個類似的截圖截個圖
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new TextView(getApplicationContext()));
}
@Override
protected void onResume() {
super.onResume();
}
}
我們看到一個 Activity 頁面最底層的 View 就是我們剛看到的 DecorView
第二部分
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
……
}
這裡看到了對 mContentParent
的賦值操作,呼叫了 generateLayout(mDecor)
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
//設定 Windows Style ,title 、action_bar 、設定鍵盤彈出方式之類的屬性
//……
//……
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
……
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
……
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
……
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
……
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
……
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
//……
//……
mDecor.finishChanging();
return contentParent;
}
這裡把 generateLayout(mDecor)
做了很大的簡化,大部分都是設定一些窗體屬性,軟鍵盤彈出方式之類的東西。我們關心的 View 相關的就以下幾行
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
mDecor.finishChanging();
layoutResource
是什麼呢?我們隨便選擇一個 R.layout.screen_simple
在 AndroidSdk 中搜到這個檔案,內容如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
這個時候再回到我們剛的那個截圖,我們找到了第二層內容 LinearLayout
的來源,這一層LinearLayout
包含兩個部分
1. id 為 action_mode_bar_stub 的 ViewStub ,用來設定 actionBar 之類的
2. id 為 android.R.id.content 的 FrameLayout。裡面會存放我們在 Activity.setContentView(resId) 傳入的檔案佈局
然後再看下最後 mDecor.finishChanging()
public void finishChanging() {
mChanging = false;
drawableChanged();
}
private void drawableChanged() {
if (mChanging) {
return;
}
//……
//……
requestLayout();
invalidate();
//……
//……
}
根據我們對 View 的瞭解,requestLayout()
和 invalidate()
會引發 View 的重新佈局和重新繪製,難道這個時候就繪製 View 了。 這不科學
而事實上,這個真的不科學。此時並不會執行繪製和計算。 原因是此時的 View 還沒有和 ViewRootImpl 關聯上 。留個懸念,這個我們在後面的章節會講解。
第三部分
第三部分就是第二部分省略的程式碼,程式碼特別長,這裡也縮減一下。
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
//……
} else {
mTitleView = (TextView)findViewById(R.id.title);
//……
}
if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(mBackgroundFallbackResource);
}
// Only inflate or create a new TransitionManager if the caller hasn't
// already set a custom one.
if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
//……
}
這裡簡單的歸納一下程式碼做的事情
1. 設定 title
2. 設定背景色
3. 處理 FEATURE_ACTIVITY_TRANSITIONS 屬性
requestLayout()
和 invalidate()
原始碼追蹤
requestLayout()
和 invalidate()
的原始碼都在 View 類裡面
先看 requestLayout()
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
我們看到此時的 View 會呼叫 mParent.requestLayout()
。mParent
會是 ViewGroup
嗎?我們看下宣告變數的地方
protected ViewParent mParent;
然後再搜下mParent
賦值的地方,發現只有一處
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}
那接下來就看 assignParent(parent)
被誰呼叫了,發現 View
中只有宣告,沒有呼叫。所以我們就去 ViewGroup
看看。發現也只有一處呼叫
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
……
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
……
}
順著這個方法追溯一下,如下圖
這時候我們又疑問了:
DecorView 的 mParent 是誰呢???
答案只有一個,是 NULL
我們剛說了 mDecor.finishChanging()
不會執行繪製和計算相。 原因是此時的 View 還沒有和 ViewRootImpl 關聯上 。
先看 invalidate()
public void invalidate() {
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
……
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
……
}
}
我們又在跟蹤 invalidate()
方法時發現了 p.invalidateChild(this, damage)
這裡似乎又是一層一層的向上迭代。為了確保,我們去看下 ViewGroup 的 invalidateChild()
public final void invalidateChild(View child, final Rect dirty) {
……
ViewParent parent = this;
if (attachInfo != null) {
……
do {
……
parent = parent.invalidateChildInParent(location, dirty);
……
}
} while (parent != null);
}
}
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
……
return mParent;
}
return null;
}
所以和 requestLayout()
一樣層層追溯,又到了 DecorView
中。我們可以準確的說 DecorView
的 mParent
其實是 ViewRootImpl
。但是怎麼證明呢???
DecorView
和 ViewRootImpl
的關係
本文開盤就已經說了 當 Activity 執行 onResume() 方法後,代表 Activity 顯示到前臺,這是為什麼呢?
我們都是 Activity
的由 ActivityManager
管理,Activity 頁面的操作必須在主執行緒中,而主執行緒就是 ActivityThread 。在 ActivityThread 的原始碼中,找到了一個 H
類,該類繼承 Handler
。在 H
的 handleMessage(Message msg)
發現以下程式碼
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
……
case RESUME_ACTIVITY:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
handleResumeActivity((IBinder) msg.obj, true, msg.arg1 != 0, true);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
……
}
然後看下 handleResumeActivity
final void handleResumeActivity(IBinder token,
……
if (r.window == null && !a.mFinished && willBeVisible) {
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 (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
……
}
這裡我們看到了 DecorView
被新增到了 ViewManager
之中。
ViewManager
只是一個介面,它的實現類為 WindowManagerImpl
。在 WindowManagerImpl
我查詢 addView()
方法
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
這裡的 mGlobal
又是 WindowManagerGlobal
的例項。所有我們又要跳轉到 WindowManagerGlobal.addView()
。
O__O "… 這時千萬別放棄,勝利就在眼前,同志們要堅持往下看啊。
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
……
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
……
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);
} ……
}
然後再看下 ViewRootImpl.setView()
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
……
view.assignParent(this);
}
}
}
親人啊!終於看到 root.setView(view, wparams, panelParentView),我們上面一直說的 View 和 ViewRootImpl 的關係終於在這關聯上了。為了更清晰一點我們畫一個時序圖
ViewRootImpl
繪製 View
現在進入了本文的壓軸部分,View 繪製的核心原始碼。
通過以上的講解,我們也知道要去找 ViewRootImpl
的 requestLayout()
和 invalidateChildInParent()
方法
ViewRootImpl.requestLayout()
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
scheduleTraversals()
又是什麼鬼
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 TraversalRunnable mTraversalRunnable = new TraversalRunnable();
mTraversalRunnable
是一個 Runnable 的子類
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
這個時候我們又要去看下 doTraversal()
的原始碼。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
最後我們找到了 performTraversals()
方法, 注意 performTraversals() 裡面有重大內容該方法很長(真的是特別長),我們這裡看一下簡化後的
private void performTraversals() {
……
if (!mStopped || mReportNextDraw) {
……
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
……
}
……
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
……
}
……
performDraw();
……
}
看到了 performMeasure
、 performLayout
、 performDraw
這裡就不用多說了吧。也就解釋了為啥 View 的繪製順序是 measure -> layout -> draw
了吧
ViewRootImpl.invalidateChildInParent()()
這裡我們不囉嗦太多,直接上原始碼
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
……
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
……
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
}
看到這裡就不用多說了,下面的執行順序 ViewRootImpl.requestLayout()
已經分析過了。
這個時候大家再看下網上很多分析 requestLayout() 和 invalidate() 方法區別的,大家可以去先去查一下,等後面有時間我也會寫一篇分析這兩個方法區別的文章。
View 到底什麼時候繪製到螢幕上?
通過以上分析我們知道
1. setContentView() 只是把 View 新增到 DecorView 上
2. onResume() 中 ViewRootImpl 和 DecorView 做了關聯
3. requestLayout() 和 invalidate() 會觸發 ViewRootImpl 繪製 View
但是!setContentView() 中呼叫了 requestLayout() 和 invalidate() 不會觸發繪製,我們上面只講了 onResume() 中 ViewRootImpl 和 DecorView 做了關聯 。到底什麼時候又呼叫了 requestLayout() 或者 invalidate() ???
往上翻我們發現在 ViewRootImpl.setView() 中有一個 requestLayout
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
……
requestLayout();
……
view.assignParent(this);
……
}
}
}
但是!居然在 view.assignParent(this)
這尼瑪逗我吧!
我們在回頭看下 requestLayout()
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
這裡重點看一下這句
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
瞭解 Android Handler Looper 都知道 postSyncBarrier 是建立一個障礙,阻止後面的 Message 物件被執行。那這裡也就解決了我剛剛的疑問, 雖然request()
在 view.assignParent(this) 之前被呼叫,但是會被阻塞。 doTraversal() 執行的時候 DecorView 和 ViewRootImpl 已經關聯了
這裡留個坑
我沒有找到 ViewRootImpl 怎麼執行到 removeSyncBarrier(mTraversalBarrier)
的程式碼。
總結
對以上內容做個總結
1. View 在 Activity 的 onCreate() 方法中通過 setContentView() 方法新增到 Activity 的 DecorView 上
2. 此時 ViewRootImpl 和 DecorView 沒有關聯上,不會繪製 View
3. 在 Activity 的 onResume() 方法執行後,DecorView 會被新增帶 ViewRootImpl 中。然後執行 requestlayout()
參考資料
相關文章
- 探究Android View 繪製流程,Canvas 的由來AndroidViewCanvas
- View的繪製二:View的繪製流程View
- View的繪製一:View是如何被新增到螢幕視窗上的View
- Android進階(五)View繪製流程AndroidView
- View 繪製流程分析View
- Android原始碼分析之View繪製流程Android原始碼View
- Android View繪製原理:繪製流程排程、測算等AndroidView
- Android View的繪製過程AndroidView
- View的繪製-measure流程詳解View
- Android View繪製流程看這篇就夠了AndroidView
- Android View 原始碼解析(三) – View的繪製過程AndroidView原始碼
- View繪製01-Android渲染系統中的ViewViewAndroid
- View繪製流程原始碼分析View原始碼
- Android自定義View之(一)View繪製流程詳解——向原始碼要答案AndroidView原始碼
- Android系統原始碼分析--View繪製流程之-inflateAndroid原始碼View
- Android系統原始碼分析–View繪製流程之-setContentViewAndroid原始碼View
- Android系統原始碼分析--View繪製流程之-setContentViewAndroid原始碼View
- Android View繪製原始碼分析 MeasureAndroidView原始碼
- 每日一問:簡述 View 的繪製流程View
- Android高階進階之路【一】Android中View繪製流程淺析AndroidView
- Android自定義view-自繪ViewAndroidView
- 2018.03.15、View 繪製流程學習 筆記View筆記
- View 的繪製過程View
- View繪製——畫多大?View
- View繪製——畫在哪?View
- Flutter 自定義繪製 ViewFlutterView
- View繪製——怎麼畫?View
- 基於原始碼分析 Android View 繪製機制原始碼AndroidView
- Android自定義View之Paint繪製文字和線AndroidViewAI
- Android自定義View之Window、ViewRootImpl和View的三大流程AndroidView
- View的繪製三:UI繪製的三大步驟ViewUI
- 不使用Activity如何新增一個ViewView
- Android View的Measure測量流程全解析AndroidView
- android 螢幕適配一:通過自定義View的方式實現適配AndroidView
- Android自定義View:View(二)AndroidView
- [Android]多層波紋擴散動畫——自定義View繪製Android動畫View
- android View 繪圖雙緩衝技術AndroidView繪圖
- View 體系詳解:View 的工作流程View