View—requestLayout、invalidate 和 postInvalidate 三者的區別

SharryChoo發表於2018-08-02

先說結論

  1. View 的 requestLayout 會回撥 onMeasure、onLayout 和 onDraw(ViewGroup.setWillNotDraw(false)的情況下) 方法
  2. invalidate 只會回撥 onDraw 方法
  3. postInvalidate 只會回撥 onDraw 方法(可以在非 UI 執行緒中回撥)

梳理一下 performTraversals

在分析三個方法之前, 先梳理一下 ViewRootImpl.performTraversals, 如果不清楚該方法的作用的話, 請先閱讀這篇博文 https://www.jianshu.com/p/2aac4e679549

/**
 * ViewRootImpl.performTraversals
 */
public void performTraversals() {
    // 1.  通過 mLayoutRequested 和 (!mStopped || mReportNextDraw) 構造 layoutRequested 變數
    boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
    
    // 2. 判斷 window 是否需要改變, 若 layoutRequested 為 false, windowShouldResize 也為 false
    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));

    // 3. 其中一個滿足即可進入這個 if 語句
    if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
        /// 3.1 判斷 View 的寬高是否有改變, 沒有的話, 不會呼叫 performMeasure
        if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            // 3.1.1 執行 performMeasure
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            // 3.1.2 改變了 layoutRequested 的值
            layoutRequested = true;
        }
    }
    
    // 4. 若執行了 performMeasure, 一般情況下也會執行 performLayout
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
        performLayout(lp, mWidth, mHeight);
    }
    
    // 5. 判斷是否取消了繪製
    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    if (!cancelDraw && !newSurface) {
        if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).startChangingAnimations();
            }
            mPendingTransitions.clear();
        }
        // 5.1 滿足條件則執行 performDraw()
        performDraw();
    } else {
        if (isViewVisible) {
            // Try again
            scheduleTraversals();
        } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).endChangingAnimations();
            }
            mPendingTransitions.clear();
        }
    }
    // Traversals 結束
    mIsInTraversal = false;
}
複製程式碼

梳理了 ViewRootImpl.performTraversals 之後, 可以看到對於 View 繪製三大流程中的前兩個,

  • performMeasure 和 performLayout 限制條件是非常多的
    • 起到決定性作用的是 mLayoutRequested 這個 Flag
    • performMeasure 執行後會將 layoutRequested 變更為 true, 不出意外的話, performLayout 會緊接著執行, 都重新測量了, 顯然要重新進行控制元件擺放
  • performDraw(); 的執行不受 mLayoutRequested 的影響, 這可能是 invalidate 只會執行 onDraw() 的原因

這裡大膽猜想一下 View.requestLayout 時 mLayoutRequested 這個 Flag 會變更為 true, 接下來帶著問題來看看 View.requestLayout 的過程

View.requestLayout

  1. 為了防止直接看 requestlayout 導致身體不適, 這裡分析 requestLayout 之前, 先分析 ViewRootImpl.performLayout() 搞清楚 layout 的兩個階段
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        /******************performLayout 的第一階段*******************/
        mLayoutRequested = false;
        mScrollMayChange = true;
        // layout 執行標記位
        mInLayout = true;
        final View host = mView;//mView 為 DecorView
        if (host == null) return;
        try {
            // 1.1 執行第一次 layout 
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            // layout結束標記位
            mInLayout = false;
            
            /******************performLayout 的第二階段*******************/
            // 檢查在進行 layout 的過程中, 是否有 View 呼叫了 requestLayout 方法
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {
                // mLayoutRequesters 就是在 mInLayout = true 的過程中 requestLayout 的 view 集合
                // 在 ViewRootImpl.requestLayoutDuringLayout 程式碼中有所體現
                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, false);
                if (validLayoutRequesters != null) {
                    // 2.1 將 mHandlingLayoutInLayoutRequest 標記為 true
                    mHandlingLayoutInLayoutRequest = true;
                    // 處理新的佈局請求
                    int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        // 在第一次 layout 執行結束後, 執行第二次 layout 請求
                        view.requestLayout();
                    }
                    // 2.2 執行二次 Measure
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    // 2.3 執行二次 layout
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
                    // 2.4 將 mHandlingLayoutInLayoutRequest 標記為 false
                    mHandlingLayoutInLayoutRequest = false;
                    // 2.5 第三次檢查 view 的 requestLayout 請求
                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    if (validLayoutRequesters != null) {
                        final ArrayList<View> finalRequesters = validLayoutRequesters;
                        getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);
                                    // 2.5.1 在第二次 layout 的過程中, 將請求 post 出去
                                    view.requestLayout();
                                }
                            }
                        });
                    }
                }

            }
        }
        // 2.5 第二次 layout 結束的標誌
        mInLayout = false;
    }
複製程式碼

可以看到在 performLayout 的過程中,

  • 第一階段: 進行正常的 layout 操作
  • 第二階段
    • 檢查 mLayoutRequesters 集合, 逐一呼叫 view.requestLayout()
    • 進行二次 Measure
    • 進行二次 Layout
    • 再檢查 mLayoutRequesters 集合, 若還有值, 則 post 到下一幀執行
  1. 再分析 View 的 requestLayout
    /**
     * View.requestLayout
     */
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout  == null) {
            ViewRootImpl viewRoot = getViewRootImpl();
            // 1. 判斷當前 ViewRootImpl 是否正在 Layout
            if (viewRoot != null && viewRoot.isInLayout()) {// isInLayout() 為 true 的條件是 mInLayout = true
                // 1.1 判斷當前 View 是否可以在 ViewRootImpl 正在進行 Layout 時, 繼續執行發起 requestLayout
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    // 返回 false , 則說明不允許, 則直接讓此次 View.requestLayout() return
                    return;
                }
            }
            // 2 將自己的狀態標記為正在 requestLayout
            mAttachInfo.mViewRequestingLayout = this;
        }
        
        // 3. 給當前 View 打上標記 PFLAG_FORCE_LAYOUT | PFLAG_INVALIDATED
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
        
        // 4. 嘗試呼叫父容器的 requestLayout
        if (mParent != null && !mParent.isLayoutRequested()) {
            // 4.1 呼叫 ViewParent 的 requestLayout, 即呼叫父容器的 requestLayout
            // ViewGroup 中並沒有重寫 requestLayout 方法, 即還會呼叫到 View 的 requestLayout 方法中去
            // 此方法會回溯到 當前 Window 的 ViewRootImpl 中的 requestLayout 中去
            mParent.requestLayout();
        }
        
        // 5. 執行到這裡, 說明當前 View 的 requestLayout 已經完成, 將 mAttachInfo.mViewRequestingLayout 置空
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
    
    /**
     * ViewRootImpl.requestLayoutDuringLayout
     */
    boolean requestLayoutDuringLayout(final View view) {
        // 1.1.1 說明傳入的 view 即為 ViewRootImpl, 因為只有它的 mParent 為null
        if (view.mParent == null || view.mAttachInfo == null) {
            // Would not normally trigger another layout, so just let it pass through as usual
            return true;
        }
        // 1.1.2 將請求 Layout 的 View 新增到 ViewRootImpl 中維護的集合 mLayoutRequesters 中
        if (!mLayoutRequesters.contains(view)) {
            mLayoutRequesters.add(view);
        }
        if (!mHandlingLayoutInLayoutRequest) {
            // 1.1.3  mHandlingLayoutInLayoutRequest 為 false
            // 說明當前 ViewRootImpl 的 performLayout() 沒有進行 second layout, 它將會在第二次 layout 中執行
            return true;
        } else {
            // 1.1.4  mHandlingLayoutInLayoutRequest 為 true
            // 說明當前 ViewRootImpl 的 performLayout() 正在進行 second layout, 此時 view 的 requestLayout 會被 post 到下一幀
            // return false; 由上面程式碼可知 View 的 requestLayout() 請求會被直接 return;
            return false;
        }
    }
    
    /**
     * View.isLayoutRequested
     */
    public boolean isLayoutRequested() {
        // 當 View 進行過 requestLayout() 時, mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        // 則會返回 true
        return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    }
    
    /**
     * ViewRootImpl.requestLayout
     * View.requestLayout() 最終的走向
     */
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            // 將當前 mLayoutRequested 標記為 true, 該 Flag 會直接影響到 performTraversals 是否執行 measure 和 layout 
            mLayoutRequested = true;
            // 這裡又重新開啟了 View 繪製的三大流程
            scheduleTraversals();
        }
    }
複製程式碼

從上述程式碼可知 View.requestLayout 方法主要做了以下事情

  • mAttachInfo.mViewRequestingLayout == null 成立時
    • 給自己打上標籤, 即讓 mAttachInfo.mViewRequestingLayout == this;
    • 判斷當前 ViewRootImpl 是否正在處理 Layout (mInLayout 是否為 true, 該變數在 performLayout 中會被改變)
      • 呼叫 ViewRootImpl.requestLayoutDuringLayout 方法來分配 view 的 requestLayout 走向
        • 將 requestLayout 的 view 新增到 ViewRootImpl 的集合 mLayoutRequesters 中
        • 根據 ViewRootImpl.mHandlingLayoutInLayoutRequest 判斷處於 performLayout() 的第幾階段
          • 若是第二階段, 則返回 false, 此時 View.reqeustlayout 請求則會被拋到下一幀執行
          • 若不是第二階段, 則返回 true, 此時 View.requestLayout 請求會在 performLayout 的第二階段執行
  • 給 View 設定 PFLAG_FORCE_LAYOUT | PFLAG_INVALIDATED 兩個 Flags
  • 嘗試呼叫 parent 的 requestLayout() 方法
    • mLayoutRequested = true; 果不其然, 這裡將這個 Flag 設定為了 true, 也印證了上面的猜想
    • 回溯到頂部, 最終會呼叫 ViewRootImpl 中重寫的 requestLayout 發起 scheduleTraversals();

View.invalidate

View.invalidate 方法會呼叫到 ViewGroup.invalidateChild 中

    /** 
     * ViewGroup.invalidateChild
     */
    public final void invalidateChild(View child, final Rect dirty) {
        final AttachInfo attachInfo = mAttachInfo;
        // child 的 parent 指定為自身
        ViewParent parent = this;
        if (attachInfo != null) {
            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }
                // 會遞迴的呼叫 parent 的 invalidateChildInParent 方法 
                parent = parent.invalidateChildInParent(location, dirty);
            } while (parent != null);
        }
    }
複製程式碼

可以看到 View.invalidate 方法會遞迴的呼叫 parent.invalidateChildInParent 方法, 直至回溯到 ViewRootImpl 中, 與 requestLayout 如出一轍, ViewRootImpl 重寫了 invalidateChildInParent 方法, 接下來看看, 它做了什麼

    /** 
     * ViewRootImpl.invalidateChildInParent
     */
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (dirty == null) {
            // 1. 直接呼叫了 invalidate 方法
            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);
            }
        }
        // 2.呼叫了 invalidateRectOnScreen 方法, 重新整理區域內的檢視
        invalidateRectOnScreen(dirty);

        return null;
    }
    
    /** 
     * ViewRootImpl.invalidate
     */
    void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            // 看到了最熟悉的 scheduleTraversals
            scheduleTraversals();
        }
    }
    
    /** 
     * ViewRootImpl.invalidateRectOnScreen
     */
    private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
            mAttachInfo.mSetIgnoreDirtyState = true;
            mAttachInfo.mIgnoreDirtyState = true;
        }
        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
        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();
        }
    }
複製程式碼

看到 ViewRootImpl 中 invalidateChildInParent 最終都回撥了 scheduleTraversals 方法, 開啟了 View 繪製的三大流程

postInvalidate

    /**
     * View.postInvalidate
     */
    public void postInvalidate() {
        postInvalidateDelayed(0);
    }
    
    /**
     * View.postInvalidateDelayed
     */
    public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            // 到 ViewRootImpl 中分發
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }
複製程式碼

可以看到 View 呼叫 postInvalidate 時, 最終會流入 ViewRootImpl 中, 接下里看看 ViewRootImpl 做了哪些操作

    /**
     * ViewRootImpl.dispatchInvalidateDelayed
     */
    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }


    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
        case MSG_INVALIDATE:
            // 在 mHandler 繫結的執行緒中呼叫了 View 的 invalidate
            ((View) msg.obj).invalidate();
            break;
    }
複製程式碼

可以看到 View.postInvalidate 本質上還是呼叫了 View.invalidate(), 它在呼叫之前會加入訊息佇列, 投遞到 Handler 建立執行緒去執行, 也就是在非 UI 執行緒我們想重新繪製時, 我們可以採用 postInvalidate 這種方式

總結

  1. View 的 requestLayout 和 invalidate 處理的方式很相似, 都是從當前 View 回溯到 ViewRootImpl 中去呼叫 scheduleTraversals 重新開啟 View 繪製的三大流程
  2. requestLayout 會通過 mLayoutRequested 這個 Flag 來控制是否執行 performMeasure 和 performLayout
  3. invalidate 時 mLayoutRequested 為 false, 故不會執行 performMeasure 和 performLayout, 只會執行 performDraw, 當然 performDraw 的執行也是有條件限制的, 不過與 mLayoutRequested 無關
  4. postInvalidate 與 invalidate 本質上沒什麼不同, 只不過 postInvalidate 可以在非 UI 建立執行緒中去通知 View 重繪, 當然了原理及是 Android 訊息機制

不得不歎服SDK開發者的技巧, 其中有很多細節都沒有兼顧到, 只是為了釐清 requestLayout, invalidate, postInvalidate 這三者的工作流程, 與部分細節, 可能有很多不到位的地方, 希望能夠批評指出

相關文章