你需要了解下Android View的更新requestLayout與重繪invalidate

guojun_fire發表於2019-03-28

在大家都瞭解過Android View的測量佈局繪製機制後,我們來細化地分析一下關於View的重繪invalidate與更新requestLayout

現象

public class CustomEmptyView extends View {
    public CustomEmptyView(Context context) {
        super(context);
    }

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

    public CustomEmptyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.i("CustomEmptyView", "onMeasure");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.i("CustomEmptyView", "onLayout");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.i("CustomEmptyView", "onDraw");
    }
}
複製程式碼

從View的繪製機制可知,View從測量、佈局、繪製的步驟中會對應執行該View#onMeasure()、View#onLayout()、View#onDraw()。那麼我們今天討論的View#invalidate()和View#requestLayout()呢?我們列印一下資料。

View#invalidate()的執行步驟是:

2019-03-26 17:32:34.739 8075-8075/com.example.myapplication I/CustomEmptyView: onDraw
複製程式碼

View#requestLayout()的執行步驟是:

2019-03-26 17:33:13.497 8075-8075/com.example.myapplication I/CustomEmptyView: onMeasure
2019-03-26 17:33:13.501 8075-8075/com.example.myapplication I/CustomEmptyView: onLayout
2019-03-26 17:33:13.503 8075-8075/com.example.myapplication I/CustomEmptyView: onDraw
複製程式碼

從列印資料來推測就是View#invalidate()只會執行View#onDraw();而View#requestLayout()則會重新走View的繪製流程。接下來我們從原始碼的角度來分析一下。 下面的原始碼分析基於android-28

View#requestLayout()

我們分析一下View#requestLayout()。我們定位到對應的原始碼

    /**
     * Call this when something has changed which has invalidated the
     * layout of this view. This will schedule a layout pass of the view
     * tree. This should not be called while the view hierarchy is currently in a layout
     * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
     * end of the current layout pass (and then layout will run again) or after the current
     * frame is drawn and the next layout occurs.
     *
     * 當某內容發生更改,導致該檢視的佈局重繪時呼叫此函式。這將安排檢視樹的佈局傳遞。
     * 當檢視層次結構當前處於佈局Layout事件時,不會執行該函式.
     * 如果正在進行佈局,可以在當前佈局傳遞結束時(然後佈局將再次執行)或在繪製當前幀並執行下一個佈局之後執行請求。
     * <p>Subclasses which override this method should call the superclass method to
     * handle possible request-during-layout errors correctly.</p>
     */
    @CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            // 該方法不在佈局事件期間中執行
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }
        // PFLAG_FORCE_LAYOUT會在執行View的measure()和layout()方法時判斷,這個是以前的文章看到的。
        // 但是在當前原始碼的View.class和ViewRootImpl.class,全域性搜尋PFLAG_FORCE_LAYOUT,並沒有直接的判斷,導致View#requestLayout()不執行測量和佈局方法
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
        // isLayoutRequested()對應是mLayoutRequested欄位,該欄位在預設為false
        if (mParent != null && !mParent.isLayoutRequested()) {
            // 執行父容器的requestLayout()方法
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
複製程式碼

當我們點選ViewGroup#requestLayout(),發現它是一個空實現,我們可知ViewParent是interface類,我們通過之前的View的分析,可以去ViewRootImpl類看看ViewGroup#requestLayout()的實現方法。

    @Override
    public void requestLayout() {
        // mHandlingLayoutInLayoutRequest這個引數,通過全域性變數定位,在performLayout()開始時為true,結束時為false,與之前說的,不在佈局期間執行相對應
        if (!mHandlingLayoutInLayoutRequest) {
            // 檢查是否UI執行緒
            checkThread();
            // 這裡將mLayoutRequested設為true
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }    
複製程式碼

看到scheduleTraversals(),我相信大家都覺得快觸控到真相,但是發現點選該方法裡的實現,並不能找到我們想要的,此時我們想一下之前的列印資料,View#requestLayout()會重新執行View的繪製步驟,View的繪製步驟最核心是ViewRootImpl#performTraverals,按照這個思路我們繼續尋找。

從上面的原始碼中,我們看到mChoreographer這個物件,我們曾經在分析滑動流程度的時候,提及過Choreographer編舞者這個物件,我們最後從mChoreographer這個物件中的mTraversalRunnable引數找到線索。

    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;
            }
        }
    }    
複製程式碼

最終我們發現mTraversalRunnable這個是一個Runnable物件,在scheduleTraversals()中傳入mTraversalRunnable,就會執行doTraversal(),在doTraversal()中我們也如願地找到我們想要的核心方法ViewRootImpl#performTraverals().當呼叫ViewRootImpl#performTraverals就重新開始該控制元件的測量、佈局、繪製步驟。符合了我們一開始的列印資料。

View#invalidate()

接著我們看一下View#invalidate的原始碼。

    /**
     * Invalidate the whole view. If the view is visible, 
     * 重繪整個View,如果View是可視的。
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     * 這個方法必須被使用在UI執行緒,用在非UI執行緒的方法為postInvalidate()
     */
    public void invalidate() {
        invalidate(true);
    }

    /**
     * This is where the invalidate() work actually happens. A full invalidate()
     * causes the drawing cache to be invalidated, but this function can be
     * called with invalidateCache set to false to skip that invalidation step
     * for cases that do not need it (for example, a component that remains at
     * the same dimensions with the same content).
     * 這就是invalidate()方法工作發生的地方,一個完整的invalidate()方法會引起繪製
     * 快取失效,但是這個函式能設定引數invalidateCache為false來跳過重繪步驟,由於
     * 該方法不被需要,例如一個元件保持相同的尺寸和相同的內容
     *
     * @param invalidateCache Whether the drawing cache for this view should be
     *            invalidated as well. This is usually true for a full
     *            invalidate, but may be set to false if the View's contents or
     *            dimensions have not changed.
     * 這繪製快取是否應被重繪.一個完整的重繪通常為true,但是可能設定為false,如果View的內容和尺寸沒有被改變。
     *
     * @hide
     */
    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 (mGhostView != null) {
            mGhostView.invalidate(true);
            return;
        }
        // 跳過重繪。從方法描述可知,該方法判斷該View不被重繪,當它處於不可見和沒有處於動畫中
        if (skipInvalidate()) {
            return;
        }
        // 這裡大量的引數對比,應該就是上面所說的判斷座標位置有沒發生變化,如果發生了變化就標識為需要重繪
        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) {
                mLastIsOpaque = isOpaque();
                mPrivateFlags &= ~PFLAG_DRAWN;
            }

            mPrivateFlags |= PFLAG_DIRTY;
            // 如果需要全部重繪,invalidate()未傳參呼叫時預設為true
            if (invalidateCache) {
                // 記住這個PFLAG_INVALIDATED標誌位
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }

            // Propagate the damage rectangle to the parent view.
            // 從下面引數mParent可知,應該是將需要重繪這個事件通知給父容器
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                // 這個就是重點,我們去看一下父容器的invalidateChild()方法
                p.invalidateChild(this, damage);
            }

            // Damage the entire projection receiver, if necessary.
            if (mBackground != null && mBackground.isProjected()) {
                final View receiver = getProjectionReceiver();
                if (receiver != null) {
                    receiver.damageInParent();
                }
            }
        }
    }

複製程式碼

從上面的分析可知,經過invalidate()的過載方法,最終會呼叫invalidateInternal(),在這個方法裡頭,要判斷是否需要重繪,如果需要重繪,就對該View進行標識,然後將該View的Rect資訊傳遞給父容器的invalidateChild().

與之前的View#requestLayout()相似,最終同樣是執行ViewRootImpl#invalidateChild(),然後我繼續分析ViewRootImpl#invalidateChild()的實現。

    @Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }

    // 由於沒有註釋,我們從方法名去分析原始碼,過載至這個最終的方法,意思為在父容器中重繪子控制元件
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        // 檢查執行緒,裡面判斷為,該方法需要執行在UI執行緒,驗證了之前View#invalidate()的描述
        checkThread();
        if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
        // 從上一步可知,dirty是不為空的
        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }
        // 這裡應該是一些座標位置的設定賦值
        if (mCurScrollY != 0 || mTranslator != null) {
            mTempRect.set(dirty);
            dirty = mTempRect;
            if (mCurScrollY != 0) {
                dirty.offset(0, -mCurScrollY);
            }
            if (mTranslator != null) {
                mTranslator.translateRectInAppWindowToScreen(dirty);
            }
            if (mAttachInfo.mScalingRequired) {
                dirty.inset(-1, -1);
            }
        }
        // 從方法的命名,這裡應該是我們需要的方法,重繪
        invalidateRectOnScreen(dirty);

        return null;
    }
    
    private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
            mAttachInfo.mSetIgnoreDirtyState = true;
            mAttachInfo.mIgnoreDirtyState = true;
        }

        // Add the new dirty rect to the current one
        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
        // Intersect with the bounds of the window to skip
        // updates that lie outside of the visible region
        final float appScale = mAttachInfo.mApplicationScale;
        final boolean intersected = localDirty.intersect(0, 0,
                (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        if (!intersected) {
            localDirty.setEmpty();
        }
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            // 上面的設定和調整,最終呼叫scheduleTraversals()
            scheduleTraversals();
        }
    }    

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }    
複製程式碼

看到這裡,我們馬上聯想起之前分析的View#requestLayout(),同樣是scheduleTraversals(),但是從列印資料可知,View#invalidate()是不會執行測量與佈局的,但是目前來看它們最終呼叫的方法可是一致的。

我們可以留意一下之前的ViewRootImpl#requestLayout()方法中,主動將一個全域性變數mLayoutRequested設定為true;那麼大膽猜測這個物件肯定會影響到performMeasure()與performLayout(),我們翻一下ViewRootImpl#performTraversals()

    private void performTraversals() {
    
    ···
        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw); 
    ···
        boolean windowShouldResize = layoutRequested && windowSizeMayChange
            && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
                || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
                        frame.width() < desiredWindowWidth && frame.width() != mWidth)
                || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
                        frame.height() < desiredWindowHeight && frame.height() != mHeight));
    ···
        if (mFirst || windowShouldResize || insetsChanged || viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
                ···
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ···
            }
        ···
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);
        }
        ···
    }
複製程式碼

對於複雜的ViewRootImpl#performTraversals(),我們抽取一些關鍵的程式碼,的確可以驗證mLayoutRequested物件是會影響測量和佈局對應的方法的,因此可以驗證我們一開始的列印資料,View#invalidate()是不會執行測量和佈局的。

View#postInvalidate()

從View#invalidate()的註釋描述可知,View#invalidate()需要執行在UI執行緒中,如果在非UI執行緒需要使用View#postInvalidate(),我們簡單地分析一下原始碼。

    public void postInvalidate() {
        postInvalidateDelayed(0);
    }
    
    public void postInvalidate(int left, int top, int right, int bottom) {
        postInvalidateDelayed(0, left, top, right, bottom);
    }
    
    public void postInvalidateDelayed(long delayMilliseconds, int left, int top,
            int right, int bottom) {

        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain();
            info.target = this;
            info.left = left;
            info.top = top;
            info.right = right;
            info.bottom = bottom;

            attachInfo.mViewRootImpl.dispatchInvalidateRectDelayed(info, delayMilliseconds);
        }
    }    
複製程式碼

ViewRootImpl#dispatchInvalidateRectDelayed()

    public void dispatchInvalidateRectDelayed(AttachInfo.InvalidateInfo info, long delayMilliseconds) {
        final Message msg = mHandler.obtainMessage(MSG_INVALIDATE_RECT, info);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }
    
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_INVALIDATE_RECT:
                final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
                info.target.invalidate(info.left, info.top, info.right, info.bottom);
                info.recycle();
                break;
                ···
        }  
    }    
複製程式碼

從原始碼可知,View#postInvalidate()會傳送一個MSG_INVALIDATE_RECT的Handler訊息,在接收訊息後,同樣是執行View#invalidate()方法。

疑問:requestLayout不執行performDraw()?

在翻閱資料的時候,很多地方都會描述到View#requestLayout不執行performDraw(),但是自己的列印結果是會顯示執行performDraw()的。我們帶著問題翻一下原始碼。直接定位到ViewRootImpl#performTraversals的performDraw()

    private void performTraversals() {
        ···
        // dispatchOnPreDraw()返回註釋是:如果當前繪製應該被取消和重新排程,則為True,否則為false。
        // final boolean isViewVisible = viewVisibility == View.VISIBLE; 只要是顯示的View,cancelDraw為true
        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

        // cancelDraw通過dispatchOnPreDraw()的註釋解析和isViewVisible,得出cancelDraw應該為false
        // newSurface預設為false,在測量判斷邏輯中,在判斷是否新的Surface會設定為true,這裡應該是false
        // 因為會執行performDraw()
        if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            performDraw();
        }
    
    }
複製程式碼

ViewRootImpl#performDraw()

    private void performDraw() {
        // Display.STATE_OFF表示顯示狀態:顯示關閉。
        // mReportNextDraw物件預設false,可在ViewRootImpl#reportNextDraw()中設定為true,但是第一個判斷已經為false,不影響判斷
        if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
            return;
        } else if (mView == null) {// 必然不為null
            return;
        }

        final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
        mFullRedrawNeeded = false;

        mIsDrawing = true;
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");

        boolean usingAsyncReport = false;
        if (mReportNextDraw && mAttachInfo.mThreadedRenderer != null
                && mAttachInfo.mThreadedRenderer.isEnabled()) {
            usingAsyncReport = true;
            mAttachInfo.mThreadedRenderer.setFrameCompleteCallback((long frameNr) -> {
                // TODO: Use the frame number
                pendingDrawFinished();
            });
        }

        try {
            // 最重點是這句,draw()方法的執行,能影響這段程式碼的執行,只有上面的兩個return邏輯。
            boolean canUseAsync = draw(fullRedrawNeeded);
            if (usingAsyncReport && !canUseAsync) {
                mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
                usingAsyncReport = false;
            }
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        ···
    }    
複製程式碼

從上面的判斷可以推斷到draw()是會被執行,與網上得出結論不一致,暫時沒得出什麼原因,如有分析錯誤和不足的地方,希望指定一下。

總結

步驟圖

View繪製步驟是performMeasure()、performLayout()、performDraw(),我們經過對invalidate和requestLayout的原始碼分析,可以得出。invalidate()方法只會執行performDraw()方法;而requestLayout()方法會執行performMeasure()、performLayout()、performDraw()。在對應的應用場景,如果只是View的顯示內容發生變化且不改變View的大小和位置,則使用invalidate(),如果大小、位置、內容都發生改變則呼叫requestLayout()。

相關文章