基于 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)用的膏潮?
這時候很多人就
好了闺兢,我們正式進入下個環(huán)節(jié)。
追蹤 onDraw() 調(diào)用棧
為了獲得 onDraw() 方法的調(diào)用情況戏罢,我們進行第一次嘗試屋谭。
AndroidStudio Find Usages
AndroidStudio 的Find Usages
功能非常強大可以迅速幫我們查找到調(diào)用 onDraw() 的地方。
但是龟糕!我們得到了 77 個結(jié)果桐磁。
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)用棧。由于屏幕有限澈蚌,堆棧的信息沒有完全截出摹芙。
通過點擊右側(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 對象的賦值代碼了该贾。
趕快看看 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寇荧。
這里還有一個疑問: 為什么嵌套了 6 層才到我們自頂一個的 View 举庶?
這里我們可以使用 AndroidStudio -> Tools -> Android -> Layout Inspector
剛好發(fā)現(xiàn)是 6 層嵌套
需要注意的是:Activity 的 ContentView 我們只放了一個簡單的 View 就已經(jīng)有 6 層嵌套了
結(jié)尾說幾句
這里只是介紹了 Android View 繪制過程中,Canvas 的賦值過程揩抡。通過 Canvas 的 Api 調(diào)用我們便可以在屏幕上繪制出各種各樣的頁面户侥。
但是,這只是 Android 繪制流程中的一小步峦嗤。如果不想追究 Canvas 的來源蕊唐,這一步甚至可以忽略。
剩下的內(nèi)容烁设,大家可以自己去閱讀源碼替梨,或者閱讀其他人的博客。如有疑問装黑,歡迎留言副瀑。