探究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 的源代碼入手基公,但是發(fā)現(xiàn) View.java 文件的源碼大概有 26488 行。這么長的代碼宋欺,直接啃下去不知道要耗費多少頭發(fā)轰豆。只好另辟蹊徑。

先看一段代碼

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. 創(chuàng)建一個 Paint(畫筆) ,設置顏色畫筆寬度祷杈。
3. 調(diào)用 canvas.drawLine(0, 0, getWidth(), getHeight(), mPaint)
   前四個參數(shù)兩兩組合斑司,代表直線的起點坐標和終點坐標。

上面的解釋看起來 特別的自然但汞,但是宿刮!好像 哪里不對互站??僵缺?胡桃?

Canvas 從哪里來?磕潮?翠胰?

看下 View 源碼,我們會回答:從父類的 draw(Canvas canvas) 方法來白愿之景!

但是!父類的 draw(Canvas canvas) 是誰調(diào)用的膏潮?

這時候很多人就

confuse.jpg

好了闺兢,我們正式進入下個環(huán)節(jié)。

追蹤 onDraw() 調(diào)用棧

為了獲得 onDraw() 方法的調(diào)用情況戏罢,我們進行第一次嘗試屋谭。

AndroidStudio Find Usages

AndroidStudio 的Find Usages 功能非常強大可以迅速幫我們查找到調(diào)用 onDraw() 的地方。

但是龟糕!我們得到了 77 個結(jié)果桐磁。

view_1.png

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

祭出萬能的 debug

靜態(tài)分析的路已經(jīng)被堵死了我擂,這時候感覺只有單步調(diào)試能迅速幫我們從 77 個結(jié)果中找到最重要的。

為了單步調(diào)試缓艳,我們需要做以下準備

1. 下載 Android 源碼校摩。如果沒有下載,當你點擊查看 View 源碼的時候 AndroidStudio 的右上角會有提示阶淘,點擊下載即可衙吩。
2. 準備虛擬機。并且虛擬機的 Android 版本和項目的 compileSdkVersion 一致溪窒。
3. 在我們自定義 View 的 onDraw() 方法中打一個斷點
4. 選擇 『Debug app』

下圖就是斷點信息坤塞,右側(cè)為斷點處的方法調(diào)用棧。由于屏幕有限澈蚌,堆棧的信息沒有完全截出摹芙。

view_2.png

通過點擊右側(cè)的方法我們可以追蹤對應的源碼。由于方法棧過長宛瞄,我們選擇從棧低開始分段梳理浮禾。

分析 onDraw() 調(diào)用棧

第一部分
  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() 方法會調(diào)用 Looper.loop() 方法阻塞線程挣轨,從而開啟整個 Android 應用(如果不阻塞,main() 方法結(jié)束整個進程也就結(jié)束)轩猩。這個線程也就是 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 以后增加的統(tǒng)一調(diào)度界面繪制機制。

此外我們發(fā)現(xiàn)方法調(diào)用棧進入了 ViewRootImpl 對象之中焦影。這時候我們需要了解 ViewRootImpl 车遂。如果不了解 ViewRootImpl 的可以先移步到Android Window 機制探索,了解一下 View 斯辰、ViewRootImpl舶担、window 之間的關系”蛏耄或者看下面兩條簡單的總結(jié)

1. Android 中所有視圖衣陶,都是在 window 上面繪制的。
2. 每個應用窗口創(chuàng)建的時候闸氮,都會創(chuàng)建一個對應的 ViewRootImpl 對象剪况。
3. ViewRootImpl 是一個根節(jié)點,負責 View 和 WindowManagerSerive 之間的通信蒲跨。

總結(jié)第二部分:接收到了一個頁面繪制消息译断,調(diào)用 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)

這里又出現(xiàn)了一個新的對象 ThreadedRenderer ,從名字是我們可以猜測和渲染頁面有關或悲。
通過 ThreadedRenderer 一系列調(diào)用

draw()  -> updateRootDisplayList()  -> updateViewTreeDisplayList() 

會調(diào)用到 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);

天啊擼8渥鳌!隆箩!我們終于看到 canvas 對象的賦值代碼了该贾。


a_ha.jpg

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

public DisplayListCanvas start(int width, int height) {
    return DisplayListCanvas.obtain(this, width, height);
}

發(fā)現(xiàn)調(diào)用了 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;
}

然后我們發(fā)現(xiàn)進入了 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捌臊,但是我們已經(jīng)知道 Canvas 是從哪里創(chuàng)建的了杨蛋。至于底層東西,有能力的時候再追蹤下去。

第四部分

經(jīng)過前面三部分的分析逞力,第四部分就比較簡單了曙寡。很容易發(fā)現(xiàn)其實就是一個循環(huán)調(diào)用。剛好對應了 View 繪制規(guī)則中的:先繪制父 View 然后再繪制子 View寇荧。

view_3.png

這里還有一個疑問: 為什么嵌套了 6 層才到我們自頂一個的 View 举庶?

這里我們可以使用 AndroidStudio -> Tools -> Android -> Layout Inspector
剛好發(fā)現(xiàn)是 6 層嵌套

需要注意的是:Activity 的 ContentView 我們只放了一個簡單的 View 就已經(jīng)有 6 層嵌套了

view_4.png

結(jié)尾說幾句

這里只是介紹了 Android View 繪制過程中,Canvas 的賦值過程揩抡。通過 Canvas 的 Api 調(diào)用我們便可以在屏幕上繪制出各種各樣的頁面户侥。

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

剩下的內(nèi)容烁设,大家可以自己去閱讀源碼替梨,或者閱讀其他人的博客。如有疑問装黑,歡迎留言副瀑。

參考

Android視圖繪制流程完全解析,帶你一步步深入了解View(二)

Zygote 進程啟動時做了哪些事恋谭?

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末俗扇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子箕别,更是在濱河造成了極大的恐慌铜幽,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件串稀,死亡現(xiàn)場離奇詭異除抛,居然都是意外死亡,警方通過查閱死者的電腦和手機母截,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門到忽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人清寇,你說我怎么就攤上這事喘漏。” “怎么了华烟?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵翩迈,是天一觀的道長。 經(jīng)常有香客問我盔夜,道長负饲,這世上最難降的妖魔是什么堤魁? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮返十,結(jié)果婚禮上妥泉,老公的妹妹穿的比我還像新娘。我一直安慰自己洞坑,他們只是感情好盲链,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著迟杂,像睡著了一般刽沾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逢慌,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天悠轩,我揣著相機與錄音间狂,去河邊找鬼攻泼。 笑死,一個胖子當著我的面吹牛鉴象,可吹牛的內(nèi)容都是我干的忙菠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼纺弊,長吁一口氣:“原來是場噩夢啊……” “哼牛欢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起淆游,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤傍睹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后犹菱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拾稳,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年腊脱,在試婚紗的時候發(fā)現(xiàn)自己被綠了访得。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡陕凹,死狀恐怖悍抑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情杜耙,我是刑警寧澤搜骡,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站佑女,受9級特大地震影響浆兰,放射性物質(zhì)發(fā)生泄漏磕仅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一簸呈、第九天 我趴在偏房一處隱蔽的房頂上張望榕订。 院中可真熱鬧,春花似錦蜕便、人聲如沸劫恒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽两嘴。三九已至,卻和暖如春族壳,著一層夾襖步出監(jiān)牢的瞬間憔辫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工仿荆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贰您,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓拢操,卻偏偏與公主長得像锦亦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子令境,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容