基於 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) 是誰呼叫的?
這時候很多人就
好了,我們正式進入下個環節。
追蹤 onDraw() 呼叫棧
為了獲得 onDraw() 方法的呼叫情況,我們進行第一次嘗試。
AndroidStudio Find Usages
AndroidStudio 的Find Usages
功能非常強大可以迅速幫我們查詢到呼叫 onDraw() 的地方。
但是!我們得到了 77 個結果。
77 個雖然不是特別多,但是也是一個不小的工作量。所以: 此路不通
祭出萬能的 debug
靜態分析的路已經被堵死了,這時候感覺只有單步除錯
能迅速幫我們從 77 個結果中找到最重要的。
為了單步除錯,我們需要做以下準備
1. 下載 Android 原始碼。如果沒有下載,當你點選檢視 View 原始碼的時候 AndroidStudio 的右上角會有提示,點選下載即可。
2. 準備虛擬機器。並且虛擬機器的 Android 版本和專案的 compileSdkVersion 一致。
3. 在我們自定義 View 的 onDraw() 方法中打一個斷點
4. 選擇 『Debug app』
複製程式碼
下圖就是斷點資訊,右側為斷點處的方法呼叫棧。由於螢幕有限,堆疊的資訊沒有完全截出。
通過點選右側的方法我們可以追蹤對應的原始碼。由於方法棧過長,我們選擇從棧低開始分段梳理。
分析 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 物件的賦值程式碼了。
趕快看看 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。
這裡還有一個疑問: 為什麼巢狀了 6 層才到我們自頂一個的 View ?
這裡我們可以使用 AndroidStudio -> Tools -> Android -> Layout Inspector
剛好發現是 6 層巢狀
需要注意的是:Activity 的 ContentView 我們只放了一個簡單的 View 就已經有 6 層巢狀了
結尾說幾句
這裡只是介紹了 Android View 繪製過程中,Canvas 的賦值過程。通過 Canvas 的 Api 呼叫我們便可以在螢幕上繪製出各種各樣的頁面。
但是,這只是 Android 繪製流程中的一小步。如果不想追究 Canvas 的來源,這一步甚至可以忽略。
剩下的內容,大家可以自己去閱讀原始碼,或者閱讀其他人的部落格。如有疑問,歡迎留言。