探究 Android View 繪製流程,Activity 的 View 如何展示到螢幕

看我眼神007發表於2019-03-04
基於 Android API 26 Platform 原始碼

寫作背景

在上一篇探究Android View 繪製流程,Xml 檔案到 View 物件的轉換過程我們瞭解了setContentView(resId) 如何把 xml 檔案轉換成 Java 中的 View 物件。本篇文章再次基礎上繼續探究,View 是如何展示到 Activity 上的。

很多 Android 開發者都知道一個事情

當 Activity 執行 onResume() 方法後,代表 Activity 顯示到前臺
複製程式碼

這句話很短,但是背後隱藏了多少方法的呼叫呢?下面我們將一層一層的剝開原始碼尋找真相。

onion.jpg

先從 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_01.png

我們看到一個 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();

        //……
        //……
       
    }
複製程式碼
nani.jpg

根據我們對 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;
    }

    ……
}
複製程式碼

順著這個方法追溯一下,如下圖

activity_view_02.png

這時候我們又疑問了:

DecorView 的 mParent 是誰呢???

question.png

答案只有一個,是 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 中。我們可以準確的說 DecorViewmParent 其實是 ViewRootImpl。但是怎麼證明呢???

DecorViewViewRootImpl 的關係

本文開盤就已經說了 當 Activity 執行 onResume() 方法後,代表 Activity 顯示到前臺,這是為什麼呢?

我們都是 Activity 的由 ActivityManager 管理,Activity 頁面的操作必須在主執行緒中,而主執行緒就是 ActivityThread 。在 ActivityThread 的原始碼中,找到了一個 H 類,該類繼承 Handler 。在 HhandleMessage(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()

keep.jpg

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 的關係終於在這關聯上了。為了更清晰一點我們畫一個時序圖

activity_view_03.png

ViewRootImpl 繪製 View

現在進入了本文的壓軸部分,View 繪製的核心原始碼。

通過以上的講解,我們也知道要去找 ViewRootImplrequestLayout()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();
        ……
 }
複製程式碼
success.jpg

看到了 performMeasureperformLayoutperformDraw 這裡就不用多說了吧。也就解釋了為啥 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) 這尼瑪逗我吧!

nani.jpg

我們在回頭看下 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 Launcher 啟動 Activity 的工作過程

Activity到底是什麼時候顯示到螢幕上的呢?

相關文章