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

基于 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 進程啟動時做了哪些事?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容