View的繪制是Android的基礎(chǔ)知識,本人將從淺入深介紹Android View的繪制流程及原理。本文基于android 12,闡述個人的理解,源碼量非常大,主要目的是記錄和分享自己的學(xué)習(xí)心得,如有錯誤,歡迎同行指正,共同進步。
1. 從onDraw說起
onDaw(Canvas canvas)這個是最簡單的繪制方法,是學(xué)習(xí)自定義控件的基本方法。canvas參數(shù)提供了繪制的畫布,我們可以重寫這個方法,來實現(xiàn)我們的繪制邏輯,比如繪制直線,繪制矩形,繪制圖片(canvas的api不是本文的范圍,后面會有專門的文章來介紹),這個方法用久了就會產(chǎn)生2個疑問,
(1)這里的canvas是那里來的?
(2)畫在canvas上的內(nèi)容怎么到屏幕上去的?
要回答這兩個問題,需要從更深入的系統(tǒng)代碼來找到答案,這是本文的主要內(nèi)容。
2 屏幕刷新流程
onDraw方法是繪制當(dāng)前View的方法,但是這個方法不需要我們手動去調(diào)用,而是在刷新屏幕的某個時刻,被系統(tǒng)調(diào)用,從而繪制出內(nèi)容,然后輸出到屏幕,這里我們就從Android java層入手來一步一步到來看看系統(tǒng)是什么時候調(diào)用到這個onDraw方法的。從源頭上看,這里涉及到兩個重要的類 Choreographer和ViewRootImpl,這兩個類都包含有很多的邏輯功能,View的繪制是其中的一種,因此不在此展開(單獨的介紹這個兩個類也是可以寫一篇文章),這也是本系列文章的原則,即從流程的角度來介紹源碼,求證單個流程的原理,而不是從代碼的角度分析某個類的全部內(nèi)容。
2.1 Vsync回調(diào)
大家都知道,當(dāng)屏幕硬件刷新之后Android低層框架會通知到應(yīng)用層,這個就是Vsync信號,Vsync信號的流程也非常復(fù)雜,因此不在此深入介紹。本文主要介紹如何接收到Vsync信號以及收到Vsync之后,是如何刷新屏幕,從而調(diào)用到onDraw進行界面繪制的。所以我們可以認(rèn)為Vsync是整個流程的起點。請看源碼:
frameworks/base/core/java/android/view/Choreographer.java
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
....
}
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
....
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
VsyncEventData vsyncEventData) {
...
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}
}
void doFrame(long frameTimeNanos, int frame,
DisplayEventReceiver.VsyncEventData vsyncEventData) {
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos, frameIntervalNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos, frameIntervalNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos, frameIntervalNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos, frameIntervalNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos, frameIntervalNanos);
}
void doCallbacks(int callbackType, long frameTimeNanos, long frameIntervalNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
...
final long now = System.nanoTime();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
...
for (CallbackRecord c = callbacks; c != null; c = c.next) {
...
c.run(frameTimeNanos);
...
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
}
private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}
上述幾段代代碼是關(guān)于刷新的,大部分的方法也比較長,我作了一些刪減,將不本文無關(guān)的代碼,日志和注釋省略,以突出流程邏輯。建議讀者理解流程后,再去詳細(xì)看每個方法的細(xì)節(jié)邏輯,有非常多的if分支,每個分支代表一種場景,另外,源碼中有很多使用日志來代替注釋的地方,這種做法我個人非常推薦,可以用于到日常的開發(fā)中去。簡單介紹以下上面的代碼
- Choreographer的構(gòu)造方法中生成了FrameDisplayEventReceiver的對象,這個對象不需要額外的注冊,而是在構(gòu)造方法內(nèi)部就注冊到底層框架,因此構(gòu)造完時就建立了Vsync信號的通道,等待Vsync到來,當(dāng)Vsync信號發(fā)出時,就會調(diào)這個FrameDisplayEventReceiver對象的onVsync方法
- onVsync方法會生成一個異步message(msg.setAsynchronous(true),異步消息如何處理的可以參看Handler相關(guān)的文章),因為這個receiver本身也是實現(xiàn)Runnable的,所以this作為這個消息的callback,并最終執(zhí)行到run方法,以及doFrame方法
- doFrame表示新的一幀可以開繪制了,這里是通過調(diào)用doCallbacks來觸發(fā)View進行重新繪制的。在Choreographer里面,總共4種callback,分別是Choreographer.CALLBACK_INPUT,Choreographer.CALLBACK_INSETS_ANIMATION,Choreographer.CALLBACK_TRAVERSAL,Choreographer.CALLBACK_COMMIT。這4種回調(diào)是按照優(yōu)先級先后進行調(diào)用,所有在一個次屏幕刷新中,最先處理的是Input回調(diào),然后是Animation回調(diào),然后是Traversal回調(diào),這個Traversal回調(diào)指的就是進行UI元素遍歷更新。而這種類型的回調(diào)是在ViewRootImpl中注冊的。這部分邏輯在ViewRootImpl中再詳細(xì)介紹。
- doCallback就是遍歷注冊的Callbacks,依次調(diào)用run方法。CalllbackRecord的run方法會判斷token == FRAME_CALLBACK_TOKEN時調(diào)用到action.doFrame方法,此時將回到ViewRootImpl中設(shè)置的回調(diào)去。每個callback處理完之后,都會被移除回收,所以次post進來的回調(diào)最多被執(zhí)行一次。
Choreographer中相關(guān)的代碼就先介紹到這里。后面進入到ViewRootImpl中繼續(xù)分析。
2.2 ViewRootImpl
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
...
mChoreographer = useSfChoreographer
? Choreographer.getSfInstance() : Choreographer.getInstance();
...
}
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
...
requestLayout();
...
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
了
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
這部分代碼是ViewRootImpl與Choreographer之間的交互,在ViewRootImpl的構(gòu)造方法中會獲取到一個Choreographer對象,他是一個線程本地變量,也就是說在同一個線程內(nèi)部,會共享一個對象。ViewRootImpl對象生成的地方不屬于本文的范圍,它是整個窗口視圖的根,在生成窗口對象后會調(diào)setView方法來設(shè)置一個View作為界面的內(nèi)容,在setView方法中會調(diào)用一次requestLayout(當(dāng)然其他情況調(diào)用到requesayout也會走后面相同的處理)此時會向Choreograher post一個Choreographer.CALLBACK_TRAVERSAL類型的callback,這個回調(diào)正是上面提到的那個回調(diào)對象,因此在sync信號到來后,會得到執(zhí)行,于是執(zhí)行mTraversalRunnable.run, 最后進入到doTravasal, 最終進入到本文的重點內(nèi)容---performTraversals方法。 可以說上面的內(nèi)容主要是介紹View繪制的前提鋪設(shè),我們理解了上述的流程就能對的View的繪制有一個全面的認(rèn)識。
3. performTravasals
了解了前面的知識之后,我們就可以放心的去看View是如何繪制出來了,performTraversals是在一幀中作的所有事情,主要來看,包含準(zhǔn)備surface ,performMeasure,performLayout和performDraw三個方法。
private void performTraversals() {
...
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_BLAST_SYNC) != 0) {
if (DEBUG_BLAST) {
Log.d(mTag, "Relayout called with blastSync");
}
reportNextDraw();
....
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
這個方法的復(fù)雜是非常高的,上面省略了各種復(fù)雜的邏輯判斷,僅列出正常情況下執(zhí)行到的主要方法。
- relayoutWindow。這個方法非常重要,他會向WMS進行通信最后生成一個surface對象,所有的繪制實質(zhì)是在surface中繪制,因此創(chuàng)建好surface是關(guān)鍵的一個操作。后續(xù)會繼續(xù)寫一些文章來分析這個relayoutWindow,在此,我們需要知道的就是surface是在這里創(chuàng)建的。應(yīng)為blast_sync是開啟的,因此會調(diào)用reportNextDraw會將mReportNextDraw = true,blast是一種新的特性,將由客戶端來負(fù)責(zé)提交framebuffer
- performMeasure 和 performLayout是執(zhí)行View的測量和布局,會分別調(diào)用到View的onMeasure和onLayout,他們分別處理的是View的尺寸和位置這兩個重要屬性,為繪制作準(zhǔn)備。因為本文是分析繪制的流程,因此不深入介紹這兩個方法。
- performDraw是執(zhí)行繪制的函數(shù),因此我們主要分析這個函數(shù)。
4. performDraw
private void performDraw() {
boolean canUseAsync = draw(fullRedrawNeeded);
if (mReportNextDraw) {
mReportNextDraw = false;
...
if (mSurfaceHolder != null && mSurface.isValid()) {
SurfaceCallbackHelper sch = new SurfaceCallbackHelper(this::postDrawFinished);
SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
}
...
}
}
private void postDrawFinished() {
mHandler.sendEmptyMessage(MSG_DRAW_FINISHED);
}
void pendingDrawFinished() {
if (mDrawsNeededToReport == 0) {
throw new RuntimeException("Unbalanced drawPending/pendingDrawFinished calls");
}
mDrawsNeededToReport--;
if (mDrawsNeededToReport == 0) {
reportDrawFinished();
} else if (DEBUG_BLAST) {
Log.d(mTag, "pendingDrawFinished. Waiting on draw reported mDrawsNeededToReport="
+ mDrawsNeededToReport);
}
}
private void reportDrawFinished() {
try {
if (DEBUG_BLAST) {
Log.d(mTag, "reportDrawFinished");
}
mDrawsNeededToReport = 0;
mWindowSession.finishDrawing(mWindow, mSurfaceChangedTransaction);
} catch (RemoteException e) {
Log.e(mTag, "Unable to report draw finished", e);
mSurfaceChangedTransaction.apply();
} finally {
mSurfaceChangedTransaction.clear();
}
}
上述代碼是執(zhí)行繪制的主要流程,他主要包括繪制和結(jié)束繪制兩部分。
- 繪制。繼續(xù)分派給draw方法去繪制具體的內(nèi)容。
- 結(jié)束。因為開啟了blast,所以在繪制結(jié)束時,需要通知到底層可以開始合成界面,這里的邏輯是通過reportDrawFinished 調(diào)用到WMS的finishDrawing方法來是實現(xiàn)的,之后屏幕上應(yīng)該就能顯示出更新后的界面元素。
如果不想詳細(xì)了解繪制的本身的邏輯話,到這里就可以算掌握了繪制主流程。 我們可以再回想一下有那些主要的流程: - Vsync信號接受
- Vsync信號處理
- ViewRootImpl開始生產(chǎn)新幀內(nèi)容
- View開始繪制
-
通過finishDrawing將新的內(nèi)容送到顯示設(shè)備。
雖然這里我們已經(jīng)對主流程已經(jīng)有一個總體的認(rèn)識,文章開頭留了2個疑問,疑問二我們可以認(rèn)為已經(jīng)找到答案了,繪制到屏幕是通過finishDrawing來實現(xiàn)的;但是我們還沒有解開文章開頭的一個疑問,canvas是哪里來的?它到底是一個什么神奇的對象?因此我們需要繼續(xù)前行。
5. draw
private boolean draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return false;
}
...
boolean useAsyncReport = false;
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty || mNextDrawUseBlastSync) {
if (isHardwareEnabled()) {
...
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}
if (animating) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return useAsyncReport;
}
在這里可以看到,draw的前提條件是存在有效的surface,這個是在relayoutWindow的時候已經(jīng)確保的了,所以方法可以繼續(xù)執(zhí)行,在接著會判斷是否開啟了硬件加速,如果開啟話,先計算后設(shè)置invalidateRoot 為true,然后通過mAttachInfo.mThreadedRender.draw(view,attachInfo,DrawCallback)來繪制這個mView。否則使用軟件繪制drawSoftware。因為現(xiàn)在基本都是使用硬件繪制,因此我們主要看看ThreadedRender是如何來draw這個View的
6. Hardware Drawing
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
attachInfo.mViewRootImpl.mViewFrameInfo.markDrawStart();
updateRootDisplayList(view, callbacks);
final FrameInfo frameInfo = attachInfo.mViewRootImpl.getUpdatedFrameInfo();
int syncResult = syncAndDrawFrame(frameInfo);
...
}
這里的代碼會稍微有一點長,部分方法并沒有省略,因此此時是比較細(xì)節(jié)的地方,盡量詳細(xì)一點。
- updateRootDisplayList。displayList是一個native的實現(xiàn),可以理解為繪制指令的集合。更新root的displayList的時候,會遍歷更新child的displayList,因此update結(jié)束了,整個UI樹就刷新了。
- syncAndDrawFrame,這個是一個native的方法,會與地層的繪制線程同步,將更新好的繪制指令在底層執(zhí)行
這個流程不是很復(fù)雜,但是updateRootDisplayList是一個比較復(fù)雜的函數(shù)。
private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");
updateViewTreeDisplayList(view);
if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {
RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);
...
canvas.drawRenderNode(view.updateDisplayListIfDirty());
...
mRootNodeNeedsUpdate = false;
...
mRootNode.endRecording();
}
}
private void updateViewTreeDisplayList(View view) {
view.mPrivateFlags |= View.PFLAG_DRAWN;
view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
== View.PFLAG_INVALIDATED;
view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
view.updateDisplayListIfDirty();
view.mRecreateDisplayList = false;
}
首先調(diào)用updateViewTreeDisplayList對UI樹上的所有元素調(diào)用view.updateDisplayListIfDirty計算需要更新的DisplayList,計算完畢后,因為mRootNodeNeedsUpdate == true,所以會對RootView進行重新繪制,這里會用RendderNode.beginRecord方法生成一個RecordingCanvas, 然后調(diào)用canvas.drawRenderNode(view.updateDisplayListIfDirty())來繪制一個RenderNode,(view.updateDisplayListIfDirty()會返回這個View已經(jīng)更新后的renderNode,RenderNode是一個繪制節(jié)點,每個View都有一個renderNode成員變量)這幾個類都是native實現(xiàn)的,暫時不分析。RecordingCanvas.drawRenderNode是這個里的關(guān)鍵方法。這里我們看到這個RecordingCanvas,我們可能會聯(lián)想到是不是就是我們要找的那個canvas,但是這里我們只看到了rootView的繪制通過canvas.drawRenderNode就直接繪制了,并沒有看到有調(diào)用rootView.onDraw(canvas)這樣的代碼。因此,我們需要進一步來看一下updateViewTreeDisplayList 這個方法,這里面有一些flag的計算
- view.mPrivateFlags |= View.PFLAG_DRAWN 走到這里先設(shè)置成已經(jīng)繪制了
- view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) == View.PFLAG_INVALIDATED,如果這個View調(diào)過invlidate,則需要重新創(chuàng)建displayList,
- view.mPrivateFlags &= ~View.PFLAG_INVALIDATED,因為計算出了mRecreateDisplayList,重置這個flag
- view.updateDisplayListIfDirty(),如果需要重新創(chuàng)建則重新創(chuàng)建displayList
- view.mRecreateDisplayList = false。已經(jīng)創(chuàng)建了,直到下次invalidate之前不需要在創(chuàng)建了。所以在后面的canvas.drawRenderNode時,再次調(diào)用view.updateDisplayListIfDirty(),view就不會再去創(chuàng)建新的dislplayList,直接返回使用之前的。我們來看看View的實現(xiàn),這里的實現(xiàn)細(xì)節(jié)值得我么細(xì)細(xì)推敲
public RenderNode updateDisplayListIfDirty() {
final RenderNode renderNode = mRenderNode;
if (!canHaveDisplayList()) {
// can't populate RenderNode, don't try
return renderNode;
}
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || !renderNode.hasDisplayList() || (mRecreateDisplayList)) {
if (renderNode.hasDisplayList()&& !mRecreateDisplayList) {
...
dispatchGetDisplayList();
return renderNode; // no work needed
}
final RecordingCanvas canvas = renderNode.beginRecording(width, height);
if (layerType == LAYER_TYPE_SOFTWARE) {
buildDrawingCache(true);
Bitmap cache = getDrawingCache(true);
if (cache != null) {
canvas.drawBitmap(cache, 0, 0, mLayerPaint);
}
} else {
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
...
} else {
draw(canvas);
}
}
return renderNode;
}
canHaveDisplayList:指的是沒有開啟硬件加速,這里肯定是false,繼續(xù)往下走
-
mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || !renderNode.hasDisplayList() || (mRecreateDisplayList)
- mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 表示緩存無效,第一次肯定true
- !renderNode.hasDisplayList() 表示沒有沒有displaylist,還沒有繪制過,第一次為true
- !mRecreateDisplayList 不需要重新創(chuàng)建 ,第一次需要創(chuàng)建,所以為false
所以這個條件為true,會進入這個case,接著又判斷renderNode.hasDisplayList() && !mRecreateDisplayList,第一次為false,所以不進入后面的case
-
因為沒有進入上面的case,所以繼續(xù)執(zhí)行,這里就到了一個關(guān)鍵的地方 final RecordingCanvas canvas = renderNode.beginRecording(width, height); 再次生成一個recordingCanvas,然后再根據(jù)當(dāng)前View layerType在軟件還是硬件進行繪制,雖然軟件繪制以及經(jīng)不常用了但是我們可以來對比一下
- 軟件繪制:首先生成一個bitmap,然后bitmap上創(chuàng)建一個canvas,將該canvas傳遞給draw或者dispatchDraw,畫完后將bitmap繪制到recordingcanvas
- 硬件繪制:直接將recordingcanvas傳遞到draw/dispatchDraw繪制。
mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW,是否需要繪制View本身,如果需要繪制,則調(diào)用draw,否則調(diào)用dispatchDraw。對一單個View而言,絕大多數(shù)是需要繪制本身的,但是對于容器類的ViewGroup,基本上是不需要繪制自身的,直接dispatchDraw去繪制children,所以這個開關(guān)位對于ViewGroup而言,默認(rèn)是開啟的,只重寫ViewGroup的onDraw,自定義的繪圖是無效果的。
標(biāo)記緩存有效,繪制完了,調(diào)用renderNode.endRecording。 這是必須的,未結(jié)束記錄的renderNode不能開啟下一次記錄。
-
如果一個View已經(jīng)繪制過了,而且之后沒有屬性發(fā)生變化,那它在之后遍歷到它時,直接使用之前的RenderNode。
- mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) != 0 表示緩存有效
- renderNode.hasDisplayList() = true,有緩存就存在displaylist
- mRecreateDisplayList = false,沒有發(fā)生變化,所以不需要重新創(chuàng)建
因此在這種情況下,就之后進入最后的else流程,什么都不做,直接返回原來的RenderNode,這樣就可以大大提升繪制的性能。
另外一種場景是,如果這個View的緩存已經(jīng)失效了,但是他自己存在DisplayList,但是還不需要重建DisplayList的時候,這個時候不需要繪制自身,但是需要去遍歷child,為那些變化了的childView重建DisplayList,即dispatchGetDisplayList()。這種情況是因為一個ViewGroup的某child的屬性變化了,因此導(dǎo)致這個child所有的父容器進行invalidate的情況,這時只需要這個child重新執(zhí)行onDraw。請看下面ViewGroup重寫的dispatchGetDisplayList方法:
protected void dispatchGetDisplayList() {
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) {
recreateChildDisplayList(child);
}
}
final int transientCount = mTransientViews == null ? 0 : mTransientIndices.size();
for (int i = 0; i < transientCount; ++i) {
View child = mTransientViews.get(i);
if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) {
recreateChildDisplayList(child);
}
}
if (mOverlay != null) {
View overlayView = mOverlay.getOverlayView();
recreateChildDisplayList(overlayView);
}
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size();
for (int i = 0; i < disappearingCount; ++i) {
final View child = disappearingChildren.get(i);
recreateChildDisplayList(child);
}
}
}
private void recreateChildDisplayList(View child) {
child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) != 0;
child.mPrivateFlags &= ~PFLAG_INVALIDATED;
child.updateDisplayListIfDirty();
child.mRecreateDisplayList = false;
}
對于ViewGroup來說,直接會管理在View層級中的children這些子控件,還有層級外的transientViews,overay,disappearingChildren, 因此分別遍歷這些控件,然后調(diào)用recreateChildDisplayList, 然后到用child.updateDisplayListIfDirty,這個方法在上面已經(jīng)介紹過了,沒有發(fā)生變化的View不會執(zhí)行任何draw
7. View.draw
從上面的分析中我看調(diào)用View.draw的地方,是在updateDisplayListIfDirtry的時候,如果這個這個View需要重新繪制,如果這個View設(shè)置的layer是LAYER_TYPE_SOFTWARE,則會使用一個普通的基于Bitmap的Canvas來傳給View來繪制,其他layer就是使用RenderNode.beginRecordings生成的RecordingCavas來繪制。但是繪制的時候,又根據(jù)是否需要繪制自身,進入到了draw和dispatchDraw,而不是直接去的onDraw。對于View來說,沒有分發(fā)的對象,他的dispatchDaw會一個空方法,因此什么都不做,因此大部分View是需要繪制的,從而進入到draw方法。
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
//draw Edge Fade is omitted here
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
}
細(xì)節(jié)代碼比較多,總的說來是分兩種場景,是否需要繪制四邊的fade效果。如果沒有fade效果就很簡單。按照這個順序執(zhí)行
- drawBackground 繪制背景
- onDraw 繪制自身,這就是我們經(jīng)常重寫的那個方法
- dispatchDraw,繪制chidren,View為空函數(shù),ViewGroup重寫該方法
- overlay.dispatchDraw 繪制overlay
- onDrawForeground,雖然方法名是drawForeground,但實質(zhì)是繪制滾動條,前景
但是如果是需要繪制fade效果的情況,在dispatchDraw之后,在繪制四邊的fade效果,之后再繪制overlay和滾動條和前景。fade的效果。
了解了View的繪制邏輯之后,我們肯定也會好奇,ViewGroup是什么情況。從前面的分析可以看到,如果ViewGroup也需要繪制自身的情況,它是和View的流程就是一樣的,否則ViewGroup的draw方法就并不會被調(diào)用,因此onDraw也不會被調(diào)用,而是直接進入到調(diào)用dispatchDraw。(當(dāng)然當(dāng)調(diào)用draw的case后面也會調(diào)用到dispatchDraw),因為我們來繼續(xù)看看ViewGroup的繪制方法dispatchDraw
protected void dispatchDraw(Canvas canvas) {
...
final ArrayList<View> preorderedList = isHardwareAccelerated() ? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
...
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
while (transientIndex >= 0) {
// there may be additional transient views after the normal views
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
...
}
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
這里的邏輯非常多,總體來看,我們需要關(guān)注幾個內(nèi)容
- 繪制順序。View添加到children的順序是物理順序,繪制順序是邏輯順序,可能是不一樣。這個順序在software繪制是是在java層調(diào)用buildOrderedChildList重新計算的,開啟硬件加速的時候,由硬件去計算。
- 確定了View的繪制順序之后,遍歷children, 但是如果該index上有transientView,而且是可見的或者有動畫則通過drawChild(transientView)繪制過渡效果,然后在獲取對應(yīng)index的child,如果child可見或者有動畫,則調(diào)用drawChild(child)繪制子控件
- 如果還有可見的transientView和disappearingView,也都通過drawChild去繪制
因此流程上已經(jīng)很清晰,但是具體如何去繪制子空間,需要繼續(xù)分析drawChild函數(shù)
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
內(nèi)部邏輯不復(fù)雜,直接調(diào)用View.draw(canvas,this, drawingTime).。這里需要注意一下,這個并不是View.draw(canvas),因為他們的參數(shù)不一樣。這就很好奇了,按照現(xiàn)有知識,直接調(diào)用child.draw(canvas)不就可以了嗎? 或者是不是簡單的重載而已?答案是NO,它是一個完全不一樣,而且非常重要且復(fù)雜的方法。我們先來看看這個View.draw(canvas,this, drawingTime)方法
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
boolean drawingWithRenderNode = mAttachInfo != null && mAttachInfo.mHardwareAccelerated&& hardwareAcceleratedCanvas;
...
if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
if (layerType != LAYER_TYPE_NONE) {
// If not drawing with RenderNode, treat HW layers as SW
layerType = LAYER_TYPE_SOFTWARE;
buildDrawingCache(true);
}
cache = getDrawingCache(true);
}
final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
if (drawingWithRenderNode) {
// Delay getting the display list until animation-driven alpha values are
// set up and possibly passed on to the view
renderNode = updateDisplayListIfDirty();
if (!renderNode.hasDisplayList()) {
// Uncommon, but possible. If a view is removed from the hierarchy during the call
// to getDisplayList(), the display list will be marked invalid and we should not
// try to use it again.
renderNode = null;
drawingWithRenderNode = false;
}
}
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((RecordingCanvas) canvas).drawRenderNode(renderNode);
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
} else if (cache != null) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
// no layer paint, use temporary paint to draw bitmap
Paint cachePaint = parent.mCachePaint;
...
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
} else {
...
canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
...
}
}
...
}
代碼邏輯非常多,我先直接總結(jié)以下,它決定這個View在作為ViewGroup的child時候如何去繪制自己。它主要也是根據(jù)是否開啟硬件加速,使用不同的緩存,如果是software drawing,它的緩存(就是上一次繪制的內(nèi)容)就是一個bitmap,如果在沒有更新的情況,直接將這個bitmap畫到畫布上。也就不再執(zhí)行onDraw進行繪制,如果沒有緩存或者屬性變化需要重建的時候,會先去建立這個緩存
private void buildDrawingCacheImpl(boolean autoScale) {
....
bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
width, height, quality);
bitmap.setDensity(getResources().getDisplayMetrics().densityDpi);
Canvas canvas;
if (attachInfo != null) {
canvas = attachInfo.mCanvas;
if (canvas == null) {
canvas = new Canvas();
}
canvas.setBitmap(bitmap);
// Temporarily clobber the cached Canvas in case one of our children
// is also using a drawing cache. Without this, the children would
// steal the canvas by attaching their own bitmap to it and bad, bad
// thing would happen (invisible views, corrupted drawings, etc.)
attachInfo.mCanvas = null;
} else {
// This case should hopefully never or seldom happen
canvas = new Canvas(bitmap);
}
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
} else {
draw(canvas);
}
這里可以看到調(diào)用View的draw或者dispatchDraw來繪制View到bitmap上。software的case分析完畢。下來看看硬件加速的情況。
if (drawingWithRenderNode) {
// Delay getting the display list until animation-driven alpha values are
// set up and possibly passed on to the view
renderNode = updateDisplayListIfDirty();
if (!renderNode.hasDisplayList()) {
// Uncommon, but possible. If a view is removed from the hierarchy during the call
// to getDisplayList(), the display list will be marked invalid and we should not
// try to use it again.
renderNode = null;
drawingWithRenderNode = false;
}
}
...
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((RecordingCanvas) canvas).drawRenderNode(renderNode);
}
這里看到,硬件加速的時候drawChild會將canvas強制轉(zhuǎn)換成RecordingCanvas然后在調(diào)用drawRenderNode(renderNode) 來將當(dāng)前的View繪制出來,而這個renderNode是上面調(diào)用updateDisplayListIfDirty()生成的。里面包含有重用DisplayList的邏輯,如果沒有更新不會調(diào)用到onDraw
最后drawChild就分析完了,其實它的部分邏輯是和updateDisplayListIfDirty差不多的,只不過drawChild是在拿到Canvas之后決定那些dirty的需要繪制到canvas,而updateDisplayListIfDirty是決定那些dirty繪制到renderNode。到這里 View.onDraw如何被執(zhí)行的就清楚了,整個繪制流程也就梳理完了。本人認(rèn)為從View/ViewGroup的角度看,雖然draw(canvas)方法決定了繪制的流水線,但drawChild和updateDisplayListIfDirty才是繪制中最關(guān)鍵的方法,它們包含的緩存邏輯對性能優(yōu)化起決定作用
8.結(jié)語
8.1 答案揭曉:
1)Canvas 是哪里來的? 在未開啟硬件加速的情況下,是直接構(gòu)造的Canvas對象,并將內(nèi)容畫到一個Bitmap緩存;在開啟硬件加速的情況下,是來自于RenderNode.begingRecording方法生成的一個RecordingCanvas,并將繪制命令記錄到DisplayList
2)繪制完畢后如何顯示在界面去?繪制完畢后,ViewRootImpl會調(diào)用reportDrawFinished呼叫WMS的遠(yuǎn)程方法finishDrawing通知繪制完畢,然后進行surfaceflinger合成后顯示到屏幕
8.2 繪制流程
-
從整體看,可以分成
- Vsync信號接收
- Vsync信號處理
- ViewRootImpl開始生產(chǎn)新幀內(nèi)容
- View開始繪制
- ViewRootImpl 通過finishDrawing將新的內(nèi)容送到顯示設(shè)備
-
從View本身來看,主要可以分成:
- drawBackground
- onDraw
- dispatchDraw
- drawEdgeEffect
- drawOverlay
- drawForeground
8.3 緩存
ViewGroup的drawChild 和 View的updateDisplayListDirty 都使用了緩存來優(yōu)化,當(dāng)一個View沒有發(fā)生變化時,直接使用上一次繪制的內(nèi)容繪制到父容器中去,這是繪制流程中重點和難點。
8.4 JNI
繪制中大量的實現(xiàn)實質(zhì)都是使用native去實現(xiàn)的,比如RenderNode,RecordingCanvas,DisplayList 等,另外也與WMS 和 SurfaceFlinger等系統(tǒng)服務(wù)關(guān)系密切。要更深入的理解繪制流程,請關(guān)注后續(xù)對JNI的一些解讀。