粗談繪制任務(wù)和繪制流程
前言
今天是2028年4月26日,天氣晴,我請(qǐng)了一天假在家陪女兒。
正在陪女兒畫畫的我,被女兒?jiǎn)柕剑?/p>
??:“爸爸,媽媽說(shuō)你的工作是可以把我們想到的東西變到手機(jī)上,是這樣嗎?”
??:“對(duì)呀,厲害吧~”
??:“那你可以把我們家的小狗狗變到手機(jī)上嗎?”
??:“當(dāng)然可以了,不過(guò)手機(jī)是很笨的東西,必須我們把所有的規(guī)則寫好,他才能聽(tīng)我們的話~”
??:“什么規(guī)則呀“
簡(jiǎn)述繪制流程
你看,手機(jī)屏幕只有這么大,所以我們先要確定狗狗的大小,該畫多大的狗狗,可以畫多大的狗狗。
這就是測(cè)量的過(guò)程。
接著,我們要確定狗狗放在哪里,左上角還是中間還是右下角?
這就是布局的過(guò)程。
最后,我們就要畫出狗狗的樣子,是斑點(diǎn)狗還是大狼狗,是小白狗還是小黑狗。
這就是繪畫的過(guò)程。
所以,在手機(jī)上變出一只狗狗,或者變出任何一個(gè)東西都需要三個(gè)步驟:
- 測(cè)量(measure)
- 布局(layout)
- 繪畫(draw)
繪制任務(wù)的來(lái)源
把視線拉回到成年人的世界。
第一次界面繪制
上篇文章說(shuō)到,當(dāng)有繪制任務(wù)的時(shí)候,會(huì)將這個(gè)任務(wù)交給Choreographer,然后再等下一個(gè)VSync信號(hào)來(lái)的時(shí)候,執(zhí)行到ViewRootImpl的performTraversals方法。
那么這個(gè)任務(wù)到底從何而來(lái)呢?回顧下Activity的顯示過(guò)程:
- 首先在setContentView方法中,創(chuàng)建了DecorView。
- 然后在handleResumeActivity方法中,執(zhí)行了addView方法將DecorView添加到WindowManager。
- 最后設(shè)置DecorView對(duì)用戶可見(jiàn)。
所以在第二步addView方法中,肯定進(jìn)行了與View繪制有關(guān)的操作:
- //WindowManagerGlobal.java
- public void addView() {
- synchronized (mLock) {
- root = new ViewRootImpl(view.getContext(), display);
- view.setLayoutParams(wparams);
- mViews.add(view);
- mRoots.add(root);
- mParams.add(wparams);
- try {
- root.setView(view, wparams, panelParentView);
- }
- }
- }
- //ViewRootImpl.java
- public void setView() {
- synchronized (this) {
- //繪制
- requestLayout();
- //調(diào)用WMS的addWindow方法
- res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
- getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
- mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
- mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
- //設(shè)置this(ViewRootImpl)為view(decorView)的parent
- view.assignParent(this);
- }
- }
- //ViewRootImpl.java
- @Override
- public void requestLayout() {
- if (!mHandlingLayoutInLayoutRequest) {
- checkThread();
- mLayoutRequested = true;
- scheduleTraversals();
- }
- }
- ->scheduleTraversals()
- ->performMeasure() performLayout() performDraw()
- ->measure、layout、draw方法
在addView方法中,創(chuàng)建了ViewRootImpl,執(zhí)行了setView方法,在這里調(diào)用了requestLayout方法開(kāi)始了View的繪制工作。
所以這里就是Activity顯示界面所做的第一次繪制來(lái)源。
那后續(xù)界面上的元素改變帶來(lái)的繪制呢?
View.requestLayout
首先看看在View中調(diào)用requestLayout方法會(huì)怎么繪制,比如TextView.setText,最后就會(huì)執(zhí)行到requestLayout
- //View.java
- public void requestLayout() {
- //設(shè)置兩個(gè)標(biāo)志位
- mPrivateFlags |= PFLAG_FORCE_LAYOUT;
- mPrivateFlags |= PFLAG_INVALIDATED;
- //執(zhí)行父view的requestLayout方法
- if (mParent != null && !mParent.isLayoutRequested()) {
- mParent.requestLayout();
- }
- }
精簡(jiǎn)之后的代碼,主要干了兩件事:
1、設(shè)置兩個(gè)標(biāo)志位,PFLAG_FORCE_LAYOUT 和 PFLAG_INVALIDATED。
2、執(zhí)行父View的requestLayout方法。
這里的標(biāo)志位暫且按下不表,待會(huì)就會(huì)遇到。從第二點(diǎn)可以看到View會(huì)一直向上執(zhí)行requestLayout方法,而頂層的View就是DecorView,DecorView的parent就是ViewRootImpl。
所以最后還是執(zhí)行到了ViewRootImpl的requestLayout方法,開(kāi)始整個(gè)View樹(shù)的 測(cè)量、布局、繪畫。
- //ViewRootImpl.java
- @Override
- public void requestLayout() {
- if (!mHandlingLayoutInLayoutRequest) {
- checkThread();
- mLayoutRequested = true;
- scheduleTraversals();
- }
- }
其中,mLayoutRequested字段設(shè)置為true,這是第二個(gè)標(biāo)志位,待會(huì)也會(huì)遇到。
但是這有點(diǎn)奇怪哦?我一個(gè)View改變了,為什么整個(gè)界面的View樹(shù)都需要重新繪制呢?
這是因?yàn)槊總€(gè)子View直接或多或少都會(huì)產(chǎn)生聯(lián)系,比如一個(gè)RelativeLayout,一個(gè)View在TextView的右邊,一個(gè)View在TextView的下面。
那么當(dāng)TextView長(zhǎng)度寬度變化了,那么其他的View自然也需要跟著變化,所以就必須整個(gè)View樹(shù)進(jìn)行重新繪制,保證布局的完整性。
View.invalidate/postInvalidate
還有一種觸發(fā)繪制的情況就是View.invalidate/postInvalidate,postInvalidate一般用于子線程,最后也會(huì)調(diào)用到invalidate方法,就不單獨(dú)說(shuō)了。
invalidate方法一般用于View內(nèi)部的重新繪畫,比如同樣是TextView.setText,也會(huì)觸發(fā)invalidate方法。
- public void invalidate(boolean invalidateCache) {
- invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
- }
- void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
- boolean fullInvalidate) {
- if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
- || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
- || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
- || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
- mPrivateFlags |= PFLAG_DIRTY;
- final ViewParent p = mParent;
- if (p != null && ai != null && l < r && t < b) {
- damage.set(l, t, r, b);
- p.invalidateChild(this, damage);
- }
- }
- }
可以看到,這里調(diào)用了invalidateInternal方法,并且傳入了可繪制的區(qū)域,最后調(diào)用了父view的invalidateChild方法。
- public final void invalidateChild(View child, final Rect dirty) {
- ViewParent parent = this;
- if (attachInfo != null) {
- do {
- parent = parent.invalidateChildInParent(location, dirty);
- } while (parent != null);
- }
- }
一個(gè)dowhile循環(huán),不斷調(diào)用父View的invalidateChildInParent方法。
也就是會(huì)執(zhí)行ViewGroup的invalidateChildInParent,最后再執(zhí)行ViewRootImpl的invalidateChildInParent方法,我們就直接看ViewRootImpl:
- //ViewRootImpl.java
- public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
- invalidateRectOnScreen(dirty);
- return null;
- }
- private void invalidateRectOnScreen(Rect dirty) {
- if (!mWillDrawSoon && (intersected || mIsAnimating)) {
- scheduleTraversals();
- }
- }
完事,果不其然,又到了scheduleTraversals繪制方法。
(這其中還有很多關(guān)于Dirty區(qū)域的繪制和轉(zhuǎn)換我省略了,Dirty區(qū)域就是需要重新繪圖的區(qū)域)
那invalidate和requestLayout有什么區(qū)別呢?繼續(xù)研究scheduleTraversals方法。
peformTraversals
接下來(lái)就看看peformTraversals方法是怎么觸發(fā)到三大繪制流程的。
- private void performTraversals() {
- boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
- //測(cè)量
- if (layoutRequested) {
- windowSizeMayChange |= measureHierarchy(host, lp, res,
- desiredWindowWidth, desiredWindowHeight);
- }
- //布局
- final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
- if (didLayout) {
- performLayout(lp, mWidth, mHeight);
- }
- //繪畫
- boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
- if (!cancelDraw) {
- performDraw();
- }
- }
我只保留了與三大繪制流程相關(guān)的直接代碼,可以看到:
1、測(cè)量過(guò)程的前提是layoutRequested為true,與mLayoutRequested有關(guān)。
2、布局過(guò)程的前提是didLayout,也與mLayoutRequested有關(guān)。
3、繪畫過(guò)程的前提是!cancelDraw。
而mLayoutRequested字段是在requestlayout方法中進(jìn)行設(shè)置的,invalidate方法中并沒(méi)有設(shè)置。所以我們可以初步斷定,只有requestLayout方法才會(huì)執(zhí)行到onMeasure和onLayout。
測(cè)量(measureHierarchy)
- private boolean measureHierarchy() {
- childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
- childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
- performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
- return windowSizeMayChange;
- }
- private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
- try {
- mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
- }
- public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
- final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
- final boolean needsLayout = specChanged
- && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
- if (forceLayout || needsLayout) {
- // first clears the measured dimension flag
- onMeasure(widthMeasureSpec, heightMeasureSpec);
- mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
- }
- }
在measure方法中,我們判斷了兩個(gè)字段forceLayout和needsLayout,當(dāng)其中有一個(gè)為true的時(shí)候,才會(huì)繼續(xù)執(zhí)行onMeasure。其中forceLayout字段代表的是mPrivateFlags標(biāo)志位是不是PFLAG_FORCE_LAYOUT。
PFLAG_FORCE_LAYOUT?是不是有點(diǎn)熟悉。剛才在View.requestLayout方法中,就對(duì)每個(gè)View都設(shè)置了這個(gè)標(biāo)志,所以才能觸發(fā)到onMeasure進(jìn)行測(cè)量。
所以requestLayout方法通過(guò)這個(gè)標(biāo)志位 PFLAG_FORCE_LAYOUT,使每個(gè)子View都能進(jìn)入到onMeasure流程。
布局(performLayout)
- private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
- int desiredWindowHeight) {
- final View host = mView;
- host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
- }
- public void layout(int l, int t, int r, int b) {
- if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
- onLayout(changed, l, t, r, b);
- }
- }
可以看到在layout方法中,是通過(guò)PFLAG_LAYOUT_REQUIRED標(biāo)記來(lái)決定是否執(zhí)行onLayout方法,而這個(gè)標(biāo)記是在onMeasure方法執(zhí)行之后設(shè)置的。
說(shuō)明了只要onMeasure方法執(zhí)行了,那么onLayout方法肯定也會(huì)執(zhí)行,這兩個(gè)方法是兄弟伙的關(guān)系,有你就有我。
繪畫(performDraw)
- private void performDraw() {
- boolean canUseAsync = draw(fullRedrawNeeded);
- }
- private boolean draw(boolean fullRedrawNeeded){
- if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
- if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
- scalingRequired, dirty, surfaceInsets)) {
- return false;
- }
- }
- return useAsyncReport;
- }
- private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
- boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
- mView.draw(canvas);
- return true;
- }
- public void draw(Canvas canvas) {
- final int privateFlags = mPrivateFlags;
- mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
- /*
- * Draw traversal performs several drawing steps which must be executed
- * in the appropriate order:
- *
- * 1. Draw the background
- * 2. If necessary, save the canvas' layers to prepare for fading
- * 3. Draw view's content
- * 4. Draw children
- * 5. If necessary, draw the fading edges and restore layers
- * 6. Draw decorations (scrollbars for instance)
- */
- // Step 1, draw the background, if needed
- drawBackground(canvas);
- // Step 2, save the canvas' layers
- canvas.saveUnclippedLayer..
- // Step 3, draw the content
- onDraw(canvas);
- // Step 4, draw the children
- dispatchDraw(canvas);
- // Step 5, draw the fade effect and restore layers
- canvas.drawRect..
- // Step 6, draw decorations (foreground, scrollbars)
- onDrawForeground(canvas);
- }
先看第二步draw(boolean fullRedrawNeeded)方法:
在該方法中,判斷了dirty是否為空,只有不為空的話才會(huì)繼續(xù)執(zhí)行下去。dirty是什么?剛才也說(shuō)過(guò),就是需要重繪的區(qū)域。
而我們調(diào)用invalidate方法的目的就是向上傳遞dirty區(qū)域,最終生成屏幕上需要重繪的dirty,requestLayout方法中并沒(méi)有對(duì)dirty區(qū)域進(jìn)行設(shè)定。
繼續(xù)看draw(Canvas canvas)方法,注釋還是比較清晰的,一共分為了六步:
- 1、繪制背景
- 2、保存圖層信息
- 3、繪制內(nèi)容(onDraw)
- 4、繪制children
- 5、繪制邊緣
- 6、繪制裝飾
而我們常用的onDraw就是用于繪制內(nèi)容。
總結(jié)
到此,View的繪制大體流程就結(jié)束了。
當(dāng)然,其中還有大量細(xì)節(jié),比如具體的繪制流程、需要注意的細(xì)節(jié)、自定義View實(shí)現(xiàn)等等,我們后面慢慢說(shuō)道。
之前我們的問(wèn)題,現(xiàn)在也可以解答了,就是繪制的兩個(gè)請(qǐng)求:requestLayout和invalidate區(qū)別是什么?
- requestLayout方法。會(huì)依次執(zhí)行performMeasure、performLayout、performDraw,但在performDraw方法中由于沒(méi)有dirty區(qū)域,一般情況下是不會(huì)執(zhí)行onDraw。也有特殊情況,比如頂點(diǎn)發(fā)生變化。
- invalidate方法。由于沒(méi)有設(shè)置標(biāo)示,只會(huì)走onDraw流程進(jìn)行dirty區(qū)域重繪。
所以如果某個(gè)元素的改變涉及到寬高布局的改變,就需要執(zhí)行requestLayout()。如果某個(gè)元素之需要內(nèi)部區(qū)域進(jìn)行重新繪制,就執(zhí)行invalidate().
如果都需要,就先執(zhí)行requestLayout(),在執(zhí)行invalidate(),比如TextView.setText()。
參考
https://www.jianshu.com/p/e79a55c141d6
https://juejin.cn/post/6904518722564653070
本文轉(zhuǎn)載自微信公眾號(hào)「碼上積木」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼上積木公眾號(hào)。