探究Android View 繪製流程,Canvas 的由來

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

寫作背景

Google 搜尋關鍵字 『android view 繪製』能得到很多資料。通常從以下幾個方面講解:

1. Measure -> layout -> draw 過程解析。
2. Paint 、Canvas 、Drawable 、Bitmap 的使用。
3. View/ViewGroup 的繪製順序。
4. View 的測量過程。
5. 自定義 View 要過載
複製程式碼

大部分文章寫的都非常棒,講的很詳細。

但是始終有一個問題一直困擾著我: View如何繪製到螢幕上!!!

所以本文重點只講 View 如何繪製到螢幕上的,其他 View 繪製流程大家自行 Google 或者參考Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二)

自定義一個簡單的 View

本應該從 View 的原始碼入手,但是發現 View.java 檔案的原始碼大概有 26488 行。這麼長的程式碼,直接啃下去不知道要耗費多少頭髮。只好另闢蹊徑。

先看一段程式碼

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Paint mPaint = new Paint();
        mPaint.setColor(0xffff0000);
        mPaint.setStrokeWidth(10);
        setContentView(new View(this) {
            @Override
            protected void onDraw(Canvas canvas) {
                canvas.drawLine(0, 0, getWidth(), getHeight(), mPaint);

            }
        });
    }
}
複製程式碼

程式碼比較簡單,我們從螢幕左上方到右下方畫了一條紅色的直線。

1. 把繪製的程式碼放在 onDraw() 方法中
2. 建立一個 Paint(畫筆) ,設定顏色畫筆寬度。
3. 呼叫 canvas.drawLine(0, 0, getWidth(), getHeight(), mPaint)
   前四個引數兩兩組合,代表直線的起點座標和終點座標。
複製程式碼

上面的解釋看起來 特別的自然,但是!好像 哪裡不對????

Canvas 從哪裡來???

看下 View 原始碼,我們會回答:從父類的 draw(Canvas canvas) 方法來啊!

但是!父類的 draw(Canvas canvas) 是誰呼叫的?

這時候很多人就

confuse.jpg

好了,我們正式進入下個環節。

追蹤 onDraw() 呼叫棧

為了獲得 onDraw() 方法的呼叫情況,我們進行第一次嘗試。

AndroidStudio Find Usages

AndroidStudio 的Find Usages 功能非常強大可以迅速幫我們查詢到呼叫 onDraw() 的地方。

但是!我們得到了 77 個結果。

view_1.png

77 個雖然不是特別多,但是也是一個不小的工作量。所以: 此路不通

祭出萬能的 debug

靜態分析的路已經被堵死了,這時候感覺只有單步除錯能迅速幫我們從 77 個結果中找到最重要的。

為了單步除錯,我們需要做以下準備

1. 下載 Android 原始碼。如果沒有下載,當你點選檢視 View 原始碼的時候 AndroidStudio 的右上角會有提示,點選下載即可。
2. 準備虛擬機器。並且虛擬機器的 Android 版本和專案的 compileSdkVersion 一致。
3. 在我們自定義 View 的 onDraw() 方法中打一個斷點
4. 選擇 『Debug app』
複製程式碼

下圖就是斷點資訊,右側為斷點處的方法呼叫棧。由於螢幕有限,堆疊的資訊沒有完全截出。

view_2.png

通過點選右側的方法我們可以追蹤對應的原始碼。由於方法棧過長,我們選擇從棧低開始分段梳理。

分析 onDraw() 呼叫棧

第一部分
  at android.os.Looper.loop(Looper.java:164)
  at android.app.ActivityThread.main(ActivityThread.java:6541)
  at java.lang.reflect.Method.invoke(Method.java:-1)
  at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
複製程式碼

Zygote 程式是所以Android程式的父程式,用來孵化Android程式。想要了解更多的可以自行 Google 或者檢視Zygote 程式啟動時做了哪些事?

ActivityThread 便是我們的 Android 程式了,其中 ActivityThread main() 方法便是我們整個Android應用程式入口之處。main() 方法會呼叫 Looper.loop() 方法阻塞執行緒,從而開啟整個 Android 應用(如果不阻塞,main() 方法結束整個程式也就結束)。這個執行緒也就是 Android 中著名的 UI 執行緒,這裡的 Looper 便是 MainLooper 。

綜上,從這部分程式碼只是 Android 程式啟動過程。但是和 View 繪製關係不大。

第二部分
  at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1386)
  at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6733)
  at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911)
  at android.view.Choreographer.doCallbacks(Choreographer.java:723)
  at android.view.Choreographer.doFrame(Choreographer.java:658)
  at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897)
  at android.os.Handler.handleCallback(Handler.java:789)
  at android.os.Handler.dispatchMessage(Handler.java:98)
複製程式碼

handler 收到並處理了一個訊息 FrameDisplayEventReceiver.run() ,表示我們收到了一個繪製頁面的訊息。其中 Choreographer 是 Android4.1 以後增加的統一排程介面繪製機制。

此外我們發現方法呼叫棧進入了 ViewRootImpl 物件之中。這時候我們需要了解 ViewRootImpl 。如果不瞭解 ViewRootImpl 的可以先移步到Android Window 機制探索,瞭解一下 View 、ViewRootImpl、window 之間的關係。或者看下面兩條簡單的總結

1. Android 中所有檢視,都是在 window 上面繪製的。
2. 每個應用視窗建立的時候,都會建立一個對應的 ViewRootImpl 物件。
3. ViewRootImpl 是一個根節點,負責 View 和 WindowManagerSerive 之間的通訊。
複製程式碼

總結第二部分:接收到了一個頁面繪製訊息,呼叫 ViewRootImpl.doTraversal() 方法。

第三部分(重點)
  at android.view.View.updateDisplayListIfDirty(View.java:18073)
  at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:643)
  at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:649)
  at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:757)
  at android.view.ViewRootImpl.draw(ViewRootImpl.java:2980)
  at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2794)
  at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2347)
複製程式碼

這裡又出現了一個新的物件 ThreadedRenderer ,從名字是我們可以猜測和渲染頁面有關。
通過 ThreadedRenderer 一系列呼叫

draw()  -> updateRootDisplayList()  -> updateViewTreeDisplayList() 
複製程式碼

會呼叫到 View.updateDisplayListIfDirty()

public RenderNode updateDisplayListIfDirty() {
    final RenderNode renderNode = mRenderNode;
    ……
    if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
            || !renderNode.isValid()
            || (mRecreateDisplayList)) {
        ……
        int width = mRight - mLeft;
        int height = mBottom - mTop;
        int layerType = getLayerType();

        final DisplayListCanvas canvas = renderNode.start(width, height);
        canvas.setHighContrastText(mAttachInfo.mHighContrastText);

        try {
            ……
        } finally {
            renderNode.end(canvas);
            setDisplayListProperties(renderNode);
        }
    } else {
        mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
    }
    return renderNode;
}
複製程式碼

這裡注意

final DisplayListCanvas canvas = renderNode.start(width, height);
複製程式碼

天啊擼!!!我們終於看到 canvas 物件的賦值程式碼了。

a_ha.jpg

趕快看看 renderNode.start(width, height)

public DisplayListCanvas start(int width, int height) {
    return DisplayListCanvas.obtain(this, width, height);
}
複製程式碼

發現呼叫了 obtain() 方法

static DisplayListCanvas obtain(@NonNull RenderNode node, int width, int height) {
    if (node == null) throw new IllegalArgumentException("node cannot be null");
    DisplayListCanvas canvas = sPool.acquire();
    if (canvas == null) {
        canvas = new DisplayListCanvas(node, width, height);
    } else {
        nResetDisplayListCanvas(canvas.mNativeCanvasWrapper, node.mNativeRenderNode,
                width, height);
    }
    canvas.mNode = node;
    canvas.mWidth = width;
    canvas.mHeight = height;
    return canvas;
}
複製程式碼

然後我們發現進入了 JNI 領域。

private DisplayListCanvas(@NonNull RenderNode node, int width, int height) {
    super(nCreateDisplayListCanvas(node.mNativeRenderNode, width, height));
    mDensity = 0; // disable bitmap density scaling
}

@CriticalNative
private static native long nCreateDisplayListCanvas(long node, int width, int height);
@CriticalNative
private static native void nResetDisplayListCanvas(long canvas, long node,
        int width, int height);
複製程式碼

遇到了 JNi 我們的追蹤也就到此為止了o(╯□╰)o,但是我們已經知道 Canvas 是從哪裡建立的了。至於底層東西,有能力的時候再追蹤下去。

第四部分

經過前面三部分的分析,第四部分就比較簡單了。很容易發現其實就是一個迴圈呼叫。剛好對應了 View 繪製規則中的:先繪製父 View 然後再繪製子 View。

view_3.png

這裡還有一個疑問: 為什麼巢狀了 6 層才到我們自頂一個的 View ?

這裡我們可以使用 AndroidStudio -> Tools -> Android -> Layout Inspector
剛好發現是 6 層巢狀

需要注意的是:Activity 的 ContentView 我們只放了一個簡單的 View 就已經有 6 層巢狀了

view_4.png

結尾說幾句

這裡只是介紹了 Android View 繪製過程中,Canvas 的賦值過程。通過 Canvas 的 Api 呼叫我們便可以在螢幕上繪製出各種各樣的頁面。

但是,這只是 Android 繪製流程中的一小步。如果不想追究 Canvas 的來源,這一步甚至可以忽略。

剩下的內容,大家可以自己去閱讀原始碼,或者閱讀其他人的部落格。如有疑問,歡迎留言。

參考

Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二)

Zygote 程式啟動時做了哪些事?

相關文章