Android invalidate/postInvalidate/requestLayout-徹底厘清

前言

系列文章:
Android Activity創建到View的顯示過程
Android Activity 與View 的互動思考
Android invalidate/postInvalidate/requestLayout-徹底厘清
Android 容易遺漏的刷新小細節

前幾篇分析了Measure、Layout、Draw 過程,這三個過程在第一次展示View的時候都會調用。那之后更改了View的屬性呢?比如更改顏色、更換文字內容、更換圖片等,還會走這三個過程嗎?循著這個思路,來分析Invalidate/RequestLayout流程。
通過本篇文章,你將了解到:

1、Invalidate 流程
2、RequestLayout 流程
3、Invalidate/RequestLayout 使用場合
4、子線程真不能繪制UI嗎
5、postInvalidate 流程

Invalidate 流程

一個小Demo

public class MyView extends View {

    private Paint paint;
    private @ColorInt int color = Color.RED;

    public MyView(Context context) {
        super(context);
        init();
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //涂紅色
        canvas.drawColor(color);
    }

    public void setColor(@ColorInt int color) {
        this.color = color;
        invalidate();
    }
}

MyView 默認展示一塊紅色的矩形區域,暴露給外界的方法:setColor
用以改變繪制的顏色。顏色改變后,需要重新執行onDraw(xx)才能看到改變后的效果,通過invalidate()方法觸發onDraw(xx)調用。
接下來看看invalidate()方法是怎么觸發onDraw(xx)方法執行的。

invalidate() 調用棧

invalidate顧名思義:使某個東西無效。在這里表示使當前繪制內容無效,需要重新繪制。當然,一般來說常常簡單稱作:刷新。
invalidate()是View.java 里的方法。

#View.java
    public void invalidate() {
        invalidate(true);
    }

    public void invalidate(boolean invalidateCache) {
        //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 (skipInvalidate()) {
            return;
        }

        //PFLAG_DRAWN 表示此前該View已經繪制過 PFLAG_HAS_BOUNDS表示該View已經layout過,確定過坐標了
        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)) {
            if (fullInvalidate) {
                //默認true
                mLastIsOpaque = isOpaque();
                //清除繪制標記
                mPrivateFlags &= ~PFLAG_DRAWN;
            }

            //需要繪制
            mPrivateFlags |= PFLAG_DIRTY;

            if (invalidateCache) {
                //1、加上繪制失效標記
                //2、清除繪制緩存有效標記
                //這兩標記在硬件加速繪制分支用到
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }
            
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                //記錄需要重新繪制的區域 damge,該區域為該View尺寸
                damage.set(l, t, r, b);
                //p 為該View的父布局
                //調用父布局的invalidateChild
                p.invalidateChild(this, damage);
            }
            ...
        }
    }

從上可知,當前要刷新的View確定了刷新區域后即調用了父布局的invalidateChild(xx)方法。該方法為ViewGroup里的final方法。

#ViewGroup.java
    public final void invalidateChild(View child, final Rect dirty) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null && attachInfo.mHardwareAccelerated) {
            //1、如果是支持硬件加速,則走該分支
            onDescendantInvalidated(child, child);
            return;
        }
        //2、軟件繪制
        ViewParent parent = this;
        if (attachInfo != null) {
            //動畫相關,忽略
            ...
            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }
                ...
                parent = parent.invalidateChildInParent(location, dirty);
                //動畫相關
            } while (parent != null);
        }
    }

由上可知,在該方法里區分了硬件加速繪制與軟件繪制,分別來看看兩者區別:

硬件加速繪制分支
如果該Window支持硬件加速,則走下邊流程:

#ViewGroup.java
    public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
        mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);
        
        if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {
           //此處都會走
            mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;
            //清除繪制緩存有效標記
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }
        
        if (mLayerType == LAYER_TYPE_SOFTWARE) {
            //如果是開啟了軟件繪制,則加上繪制失效標記
            mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
            //更改target指向
            target = this;
        }

        if (mParent != null) {
            //調用父布局的onDescendantInvalidated
            mParent.onDescendantInvalidated(this, target);
        }
    }

onDescendantInvalidated 方法的目的是不斷向上尋找其父布局,并將父布局PFLAG_DRAWING_CACHE_VALID 標記清空,也就是繪制緩存清空。
而我們知道,根View的mParent指向ViewRootImpl對象,因此來看看它里面的onDescendantInvalidated()方法:

#ViewRootImpl.java
    @Override
    public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
        // TODO: Re-enable after camera is fixed or consider targetSdk checking this
        // checkThread();
        if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
            mIsAnimating = true;
        }
        invalidate();
    }

    @UnsupportedAppUsage
    void invalidate() {
        //mDirty 為臟區域,也就是需要重繪的區域
        //mWidth,mHeight 為Window尺寸
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            //開啟View 三大流程
            scheduleTraversals();
        }
    }

做個小結:

1、invalidate() 對于支持硬件加速來說,目的就是尋找需要重繪的View。當前View肯定是需要重繪的,繼續遞歸尋找其父布局直至到根View。
2、如果該View需要重繪,則加上PFLAG_INVALIDATED 標記。
3、設置重繪區域。

用圖表示硬件加速繪制的invaldiate流程:


軟件繪制分支
如果該Window不支持硬件加速,那么走軟件繪制分支:
parent.invalidateChildInParent(location, dirty) 返回mParent,只要mParent不為空那么一直調用invalidateChildInParent(xx),實際上這也是遍歷ViewTree過程,來看看關鍵invalidateChildInParent(xx):

#ViewGroup.java
    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        //dirty 為失效的區域,也就是需要重繪的區域
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
            //該View繪制過或者繪制緩存有效
            if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
                    != FLAG_OPTIMIZE_INVALIDATE) {
                //修正重繪的區域
                dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                        location[CHILD_TOP_INDEX] - mScrollY);
                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                    //如果允許子布局超過父布局區域展示
                    //則該dirty 區域需要擴大
                    dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
                }
                final int left = mLeft;
                final int top = mTop;
                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                    //默認會走這
                    //如果不允許子布局超過父布局區域展示,則取相交區域
                    if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
                        dirty.setEmpty();
                    }
                }
                //記錄偏移,用以不斷修正重繪區域,使之相對計算出相對屏幕的坐標
                location[CHILD_LEFT_INDEX] = left;
                location[CHILD_TOP_INDEX] = top;
            } else {
                ...
            }
            //標記緩存失效
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            if (mLayerType != LAYER_TYPE_NONE) {
                //如果設置了緩存類型,則標記該View需要重繪
                mPrivateFlags |= PFLAG_INVALIDATED;
            }
            //返回父布局
            return mParent;
        }
        return null;
    }

與硬件加速繪制一致,最終調用ViewRootImpl invalidateChildInParent(xx),來看看實現:

#ViewRootImpl.java
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
        if (dirty == null) {
            //臟區域為空,則默認刷新整個窗口
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }
        ...
        invalidateRectOnScreen(dirty);
        return null;
    }

    private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        //合并臟區域,取并集
        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
        ...
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            //開啟View的三大繪制流程
            scheduleTraversals();
        }
    }

做個小結:

1、invalidate() 對于軟件繪制來說,目的就是尋找需要重繪的區域。
2、確定重繪的區域在Window里的位置,該區域需要重新繪制。

用圖表示軟件繪制invalidate流程:


image.png

上述分析了硬件加速繪制與軟件繪制時invalidate的不同,它們的最終目的都是為了重走Draw過程。重走Draw過程通過調用scheduleTraversals() 觸發的,來看看是如何觸發的。

想了解更多硬件加速繪制請移步:
Android 自定義View之Draw過程(中)

觸發Draw過程
scheduleTraversals 詳細分析在這篇文章:
Android Activity創建到View的顯示過程

三大流程真正開啟在ViewRootImpl->performTraversals(),在該方法里根據一定的條件執行了Measure(測量)、Layout(擺放)、Draw(繪制)。
本次著重分析如何觸發Draw過程。

#ViewRootImpl.java
    private void performDraw() {
        ...
        try {
            //調用draw 方法
            boolean canUseAsync = draw(fullRedrawNeeded);
            ...
        } finally {
            mIsDrawing = false;
        }
        ...
    }

    private boolean draw(boolean fullRedrawNeeded) {
        //mSurface 在ViewRootImpl 構建的時候創建
        Surface surface = mSurface;
        if (!surface.isValid()) {
            return false;
        }
        ...
        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            //invalidate 時,dirty就已經被賦值
            //滿足其中一個條件即可,重點關注第一個條件
            if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
                ...
                //硬件加速繪制
                mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
            } else {
                ...
                //軟件繪制
                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) {
        final Canvas canvas;
        try {
            //dirty 為需要繪制的區域
            //invalidate 指定的刷新區域會影響canvas繪制區域
            canvas = mSurface.lockCanvas(dirty);
        } catch (Surface.OutOfResourcesException e) {
            return false;
        } catch (IllegalArgumentException e) {
            return false;
        } finally {
        }
        try {
            //ViewTree 開始繪制
            mView.draw(canvas);
        } finally {
            ..
        }
        return true;
    }

可以看出,invalidate 最終觸發了Draw過程。

1、不管是硬件加速繪制還是軟件繪制,都會設置重繪的矩形區域。對于硬件加速繪制來說,重繪的區域為整個Window的大小。而對于軟件繪制則是設置相交的矩形區域。
2、只要重繪區域不為空,那么當開啟三大流程時,Draw過程必然被調用。
3、對于硬件加速繪制來說,通過繪制標記控制需要重繪的View,因此當我們調用view.invalidate()時,該view被設置了重繪標記,在Draw過程里該view draw(xx)被調用。當然如果其父布局設置了軟件緩存,則其父布局也需要被重繪,父布局下的子布局也需要重繪。
4、對于軟件繪制來說,整個ViewTree的Draw過程都會被調用,只是Canvas僅僅繪制重繪區域指定的矩形區域。

可以看出,啟用硬件加速繪制可以避免不必要的繪制。
關于硬件加速繪制與軟件繪制詳細區別,請移步系列文章:
Android 自定義View之Draw過程(上)

最后,用圖表示invalidate流程:


image.png

RequestLayout 流程

顧名思義,重新請求布局。
來看看View.requestLayout()方法:

#View.java
    public void requestLayout() {
        //清空測量緩存
        if (mMeasureCache != null) mMeasureCache.clear();
        ...
        //添加強制layout 標記,該標記觸發layout
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        //添加重繪標記
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            //如果上次的layout 請求已經完成
            //父布局繼續調用requestLayout
            mParent.requestLayout();
        }
        ...
    }

可以看出,這個遞歸調用和invalidate一樣的套路,向上尋找其父布局,一直到ViewRootImpl為止,給每個布局設置PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED標記。
查看ViewRootImpl requestLayout()

#ViewRootImpl.java
    public void requestLayout() {
        //是否正在進行layout過程
        if (!mHandlingLayoutInLayoutRequest) {
            //檢查線程是否一致
            checkThread();
            //標記有一次layout的請求
            mLayoutRequested = true;
            //開啟View 三大流程
            scheduleTraversals();
        }
    }

很明顯,requestLayout目的很單純:

1、向上尋找父布局、并設置強制layout標記
2、最終開啟三大繪制流程

和invalidate一樣的配方,當刷新信號來到之時,調用doTraversal()->performTraversals(),而在performTraversals()里真正執行三大流程。

#ViewRootImpl.java
    private void performTraversals() {
        //mLayoutRequested 在requestLayout時賦值為true
        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
        if (layoutRequested) {
            //measure 過程
            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
        }
        ...

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        if (didLayout) {
            //layout 過程
            performLayout(lp, mWidth, mHeight);
        }
        ...
    }

由此可見:

1、requestLayout 最終將會觸發Measure、Layout 過程。
2、由于沒有設置重繪區域,因此Draw 過程將不會觸發。

之前設置的PFLAG_FORCE_LAYOUT標記有啥用呢?
回憶一下measure 過程:

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        //requestLayout時,PFLAG_FORCE_LAYOUT 標記被設置
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        ...
        if (forceLayout || needsLayout) {
            ...
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                //測量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
            } else {
                ...
            }
            ...
            }
        }
    }

PFLAG_FORCE_LAYOUT 標記打上之后,會觸發onMeasure()測量自身及其子布局。

試想一下,假設View的尺寸改變了,變大了,那么調用了requestLayout后因為走了Measure、Layout 過程,測量、擺放倒是重新設置了,但是不調用Draw出不來效果啊。實際上,View layout時候已經考慮到了。
在View.layout(xx)->setFrame(xx)里

#View.java
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            ...
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            //尺寸發生改變 調用invalidate 傳入true,否則傳入false
            invalidate(sizeChanged);
            ...
        }
        ...
        return changed;
    }

也就是說:

1、requestLayout調用后,可能會觸發invalidate。
2、若是觸發了invalidate(),不管傳入true還是false,都會走重繪流程。

關于measure、layout 過程更深入的分析,請移步:

用圖表示requestLayout過程:


image.png

Invalidate/RequestLayout 使用場合

結合requestLayout和invalidate與View三大流程關系,有如下圖:


image.png

總結一下:

1、invalidate調用后只會觸發Draw 過程。
2、requestLayout 會觸發Measure、Layout過程,如果尺寸發生改變,則會調用invalidate。
3、當涉及View的尺寸、位置變化時使用requestLayout。
4、當僅僅需要重繪時調用invalidate。
5、如果不確定requestLayout 是否觸發invalidate,可在requestLayout后繼續調用invalidate。

上面僅僅說明了單個布局Invalidate/RequestLayout聯系,那么如果父布局調用了invalidate,那么子布局會走重繪過程嗎?接下來列舉這些關系。

子布局/父布局 Invalidate/RequestLayout 關系

子布局Invalidate
如果是軟件繪制或者父布局開啟了軟件緩存繪制,父布局會走重繪過程(前提是WILL_NOT_DRAW標記沒設置)。

子布局RequestLayout
父布局會重走Measure、Layout過程。

父布局Invalidate
如果是軟件繪制,則子布局會走重繪過程。

父布局RequestLayout
如果父布局尺寸發生了改變,則會觸發子布局Measure過程、Layout過程。

子線程真不能繪制UI嗎

在Activity onCreate里創建子線程并展示對話框:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_group);

        new Thread(new Runnable() {
            @Override
            public void run() {
                TextView textView = new TextView(MainActivity.this);
                textView.setText("hello thread");
                Looper.prepare();
                Dialog dialog = new Dialog(MainActivity.this);
                dialog.setContentView(textView);
                dialog.show();
                Looper.loop();
            }
        }).start();
    }

答案是可以的,接下來分析為什么可以。

在分析ViewRootImpl里requestLayout/invalidate過程中,發現其內部調用了checkThread()方法:

#ViewRootImpl.java
    void checkThread() {
        //當前調用線程與mThread不是同一線程則會拋出異常
        if (mThread != Thread.currentThread()) {
            //簡單翻譯來說:只有創建了ViewTree的線程才能操作里邊的View
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

問題的關鍵是mThread是什么?從哪里來?

#ViewRootImpl.java
    public ViewRootImpl(Context context, Display display) {
        ...
        //mThread 為Thread類型
        //也就是說哪個線程執行了構造ViewRootImpl對象,那么mThread就是指向那個線程
        mThread = Thread.currentThread();
        ...
    }

而創建ViewRootImpl對象是在調用WindowManager.addView(xx)過程中創建的。
關于WindowManager/Window 請移步:Window/WindowManager 不可不知之事

現在回過頭來看Dialog創建就比較明朗了:

1、dialog.show() 調用WindowManager.addView(xx),此時是子線程調用,因此ViewRootImpl對象是在子線程調用的,進而mThread指向子線程。
2、當ViewRootImpl對象構建成功后,調用其setView(xx)方法,里面調用了requestLayout,此時還是子線程。
3、checkThread()判斷是同一線程,因此不會拋出異常。

實際上,"子線程不能更新ui" 更合理的表述應為:View只能被構建了ViewTree的線程操作。只是通常來說,Activity 構建ViewTree的線程被稱作UI(主)線程,因此才會有上述說法。

postInvalidate 流程

既然invalidate()只能主線程調用(硬件加速條件下,不調用checkThread()),那如果想在子線程調用呢?當然想到的是先通過Handler切換到主線程,再執行invalidate(),但是每次這么寫有點冗余,幸好,View里提供了postInvalidate:

#View.java
    public void postInvalidate() {
        postInvalidateDelayed(0);
    }

    public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            //還是靠ViewRootImpl
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

切到ViewRootImpl.java

#ViewRootImpl.java
    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        //此處Message.obj = view
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_INVALIDATE:
                //obj 即為待刷新的View
                ((View) msg.obj).invalidate();
                break;
                ...
        }
    }

發現了真相:

postInvalidate 通過ViewRootImpl 里的handler切換到UI線程,最終執行
invalidate()。
ViewRootImpl 里的hanlder綁定的線程即是UI線程。

本文基于Android 10.0

如果您喜歡,請點贊/關注,您的鼓勵是我前進的動力。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數的可能。 ...
    yichen大刀閱讀 6,099評論 0 4
  • 公元:2019年11月28日19時42分農歷:二零一九年 十一月 初三日 戌時干支:己亥乙亥己巳甲戌當月節氣:立冬...
    石放閱讀 6,913評論 0 2
  • 今天上午陪老媽看病,下午健身房跑步,晚上想想今天還沒有斷舍離,馬上做,衣架和旁邊的的布衣架,一看亂亂,又想想自己是...
    影子3623253閱讀 2,927評論 3 8