安卓invalidate()、postInvalidate()、requestLayout()源碼分析

  • 最近在擼Golang有點上火了,來整理下安卓源碼資料???
  • 分析結(jié)果基于Audroid API 26

requestLayout()源碼分析

  • 假如在一個頁面上有個按鈕,點擊按鈕就對一個 view.requestLayout(),這個 view 執(zhí)行的方法如下:
InvalidateTextView------onMeasure
InvalidateTextView------onMeasure
InvalidateTextView-------layout
InvalidateTextView--------onLayout
InvalidateTextView----------draw
InvalidateTextView------------onDraw
  • view.requestLayout() 方法的詳情
  @CallSuper
    public void requestLayout() {
        // 清除繪制的緩存
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            //只有在布局邏輯中觸發(fā)請求,如果這是請求它的視圖,而不是其父層次結(jié)構(gòu)中的視圖
            ViewRootImpl viewRoot = getViewRootImpl();
            //如果連續(xù)請求兩次,其中一次自動返回!
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }
       //todo   為當(dāng)前view設(shè)置標記位 PFLAG_FORCE_LAYOUT
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
           //   todo  向父容器請求布局 這里是向父容器請求布局,即調(diào)用父容器的requestLayout方法,為父容器添加PFLAG_FORCE_LAYOUT標記位,而父容器又會調(diào)用它的父容器的requestLayout方法,即requestLayout事件層層向上傳遞,直到DecorView,即根View,而根View又會傳遞給ViewRootImpl,也即是說子View的requestLayout事件,最終會被ViewRootImpl接收并得到處理
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
  • 1、如果緩存不為null,清除繪制的緩存
        if (mMeasureCache != null) mMeasureCache.clear();
  • 2、這里判斷了是否在layout,如果是,就返回,也就可以理解為: 如果連續(xù)請求兩次,并且其中的一次正在layout中,其中一次返回!這樣做是節(jié)約性能
   if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            //只有在布局邏輯中觸發(fā)請求,如果這是請求它的視圖,而不是其父層次結(jié)構(gòu)中的視圖
            ViewRootImpl viewRoot = getViewRootImpl();
            //如果連續(xù)請求兩次,其中一次自動返回!
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }
  • 3、 為當(dāng)前view設(shè)置標記位PFLAG_FORCE_LAYOUT,關(guān)于 |=符號:a|=b的意思就是把a和b按位或然后賦值給a 按位或的意思就是先把a和b都換成2進制,然后用或操作,相當(dāng)于a=a|b
   mPrivateFlags |= PFLAG_FORCE_LAYOUT;
   mPrivateFlags |= PFLAG_INVALIDATED;
  • 4、向父容器請求布局,即調(diào)用ViewGroup父容器的requestLayout()方法,為父容器添加PFLAG_FORCE_LAYOUT標記位,而父容器又會調(diào)用它的父容器的requestLayout()方法,即requestLayout()事件層層向上傳遞,直到DecorView,即根View,而根View又會傳遞給ViewRootImpl,也即是說子View的requestLayout()f事件,最終會被ViewRootImpl.requestLayout()接收并得到處理
      if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
  • 5、ViewRootImpl.requestLayout()方法詳情

    @Override
     public void requestLayout() {
         if (!mHandlingLayoutInLayoutRequest) {
             // 檢查是否在主線程,不在的話,拋出異常
             checkThread();
             mLayoutRequested = true;
             scheduleTraversals();
         }
     }
    
    
    • 1、檢查是否在主線程,不在的話,拋出異常checkThread();
    void checkThread() {
       if (mThread != Thread.currentThread()) {
           throw new CalledFromWrongThreadException(
                   "Only the original thread that created a view hierarchy can touch its views.");
       }
    }
    
    • 2 、最終走到這個方法來ViewRootImpl.scheduleTraversals(),在其中可以看到一行非常有意思的代碼
      mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); ,其中有個對象mTraversalRunnable,這樣下去就會重新的測量、布局和繪制;具體的流程可以看這篇文章Android源碼分析(View的繪制流程)
       // requestLayout()  會調(diào)用這個方法 
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 最終調(diào)用的是這個方法
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
    
  • 有個問題,我先拋出結(jié)論,requessLayout() 、invalidate()、postInvalidate()最終的底層調(diào)用的是 ViewRootImpl.scheduleTraversals()的方法,為什么僅僅requessLayout()才會執(zhí)行onMeasure() onLayout() onDraw()這幾個方法?

  • 關(guān)于view.measure()方法:在前面我們知道 mPrivateFlags |= PFLAG_FORCE_LAYOUT 所以 forceLayout = true,也就是會執(zhí)行onMeasure(widthMeasureSpec, heightMeasureSpec);,同時執(zhí)行完了以后呢,最后為標記位設(shè)置為mPrivateFlags |=PFLAG_LAYOUT_REQUIRED

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        // requestLayout的方法改變的  mPrivateFlags |= PFLAG_FORCE_LAYOUT; 所以 forceLayout = true
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        ...
        if (forceLayout || needsLayout) {
        ...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                //最終會走到這方法來
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } 
            // 接著最后為標記位設(shè)置為PFLAG_LAYOUT_REQUIRED
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
       ...
    }
  • 關(guān)于view.layout()方法:判斷標記位是否為PFLAG_LAYOUT_REQUIRED,如果有,則對該View進行布局,也就是走到onLayout(changed, l, t, r, b);,最后清除標記mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
 @SuppressWarnings({"unchecked"})
    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            //第二次調(diào)用這個方法,,,
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
           //判斷標記位是否為PFLAG_LAYOUT_REQUIRED,如果有,則對該View進行布局
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
            //  onLayout方法完成后,清除PFLAG_LAYOUT_REQUIRED標記位
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
        }
    //  //最后清除PFLAG_FORCE_LAYOUT標記位
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
       ...
    }
  • 以上就是 requestLayout()的分析的結(jié)果:view調(diào)用了這個方法,其實會從view樹重新進行一次測量、布局、繪制這三個流程。
  • 做了一張圖


    requestLayout()的原理.jpg

invalidate()源碼分析

  • view.invalidate();繼承一個Textview,然后重寫方法,設(shè)置一個but,同時請求方法,打印日志:請求一次的輸出的結(jié)果
InvalidateTextView----------draw
InvalidateTextView------------onDraw
  • 方法詳情 : view.invalidate()
  public void invalidate() {
        invalidate(true);
    }
  • 該視圖的繪圖緩存是否也應(yīng)無效。對于完全無效,設(shè)置為true,但是如果視圖的內(nèi)容或維度沒有改變,則可以設(shè)置為false。
   public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
  • invalidateInternal()方法詳情:其實關(guān)鍵的方法就是invalidateChild()

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
             boolean fullInvalidate) {
         if (mGhostView != null) {
             mGhostView.invalidate(true);
             return;
         }
         // 判斷是否可見,是否在動畫中,是否不是ViewGroup,三項滿足一項,直接返回
         if (skipInvalidate()) {
             return;
         }
    //根據(jù)View的標記位來判斷該子View是否需要重繪,假如View沒有任何變化,那么就不需要重繪
         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;
             }
             //設(shè)置PFLAG_DIRTY標記位
             mPrivateFlags |= PFLAG_DIRTY;
    
             if (invalidateCache) {
                 mPrivateFlags |= PFLAG_INVALIDATED;
                 mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
             }
             //把需要重繪的區(qū)域傳遞給父容器
             // Propagate the damage rectangle to the parent view.
             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);
                 //調(diào)用父容器的方法,向上傳遞事件
                 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();
                 }
             }
         }
     }
    
    • 1、判斷是否可見,是否在動畫中,是否不是ViewGroup,三項滿足一項,直接返回,這個方法也可以知道,invalidate()針對的是View,而不是ViewGroup
    private boolean skipInvalidate() {
        return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
                (!(mParent instanceof ViewGroup) ||
                        !((ViewGroup) mParent).isViewTransitioning(this));
    }
    
    • 2、通過View的標記位來判斷孩子View是否需要重新繪制,如果沒有變化的話,那么就不需要重新繪制
    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)
    
    • 3、需要重新繪制的區(qū)域傳遞給父容器,向上傳遞事件,記住在這里 damage這個變量肯定不為null,要不然在這個方法里面就會直接拋出空指針異常。
    p.invalidateChild(this, damage);
    
    • 4、損壞整個投影接收機,如果不需要。mBackground.isProjected(): 這張畫是否需要投影。
     if (mBackground != null && mBackground.isProjected()) {
               final View receiver = getProjectionReceiver();
               if (receiver != null) {
                   receiver.damageInParent();
               }
           }
    
  • 關(guān)鍵的方法:ViewRootImpl.invalidateChild(this, damage);
  @Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }
  • invalidateChildInParent(null, dirty);進行了offset和union對坐標的調(diào)整,然后把dirty區(qū)域的信息保存在mDirty中,最后調(diào)用了scheduleTraversals()方法,觸發(fā)View的工作流程,由于沒有添加measure和layout的標記位,因此measure、layout流程不會執(zhí)行,而是直接從draw流程開始.

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
     // 檢查線程,不是ui線程,直接拋出異常
     checkThread();
      if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + 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中的坐標轉(zhuǎn)化為父容器中的坐標,考慮mScrollX和mScrollY的影響
             dirty.offset(0, -mCurScrollY);
         }
         if (mTranslator != null) {
             mTranslator.translateRectInAppWindowToScreen(dirty);
         }
         if (mAttachInfo.mScalingRequired) {
             dirty.inset(-1, -1);
         }
      }
      //進行了offset和union對坐標的調(diào)整
      invalidateRectOnScreen(dirty);
    
      return null;
    }
    
    • 1、檢查線程,不是ui線程,直接拋出異常.和requestLayout()一樣的
       checkThread();
    
    • 2、如果是從 invalidate() 方法過來的話,那么dirty 肯定不為null 因為要是為null的話,前面調(diào)用方法的地方就拋出了空指針的異常
     if (dirty == null) {
         invalidate();
         return null;
     }
    
    • 3、通過將dx=0添加到其左、右坐標,并將 mCurScrollY 添加到其頂部和底部坐標來抵消矩形。
     dirty.offset(0, -mCurScrollY);
    
    • 4、進行了offset和union對坐標的調(diào)整
    invalidateRectOnScreen(dirty);
    
  • 關(guān)于invalidateRectOnScreen(dirty)方法:最終的關(guān)鍵的方法: scheduleTraversals();
    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
        // 添加一個新的 dirty rect 給當(dāng)前的Rect
        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();
        }
    }
  • 最終的關(guān)鍵的方法:ViewRootImpl.scheduleTraversals();,也就是會調(diào)用到這個對象mTraversalRunnable;也就是和requessLaout()最終調(diào)用的底層的方法一樣,只不過對于invalidate()沒有添加measure()layout()的標記位,后面的流程也就不會執(zhí)行!具體的流程可以看這篇文章Android源碼分析(View的繪制流程)
  • 該方法的調(diào)用會引起View樹的重繪,常用于內(nèi)部調(diào)用(比如 setVisiblity())或者需要刷新界面的時候,需要在主線程(即UI線程)中調(diào)用該方法,invalidate有多個重載方法,但最終都會調(diào)用invalidateInternal方法,在這個方法內(nèi)部,進行了一系列的判斷,判斷View是否需要重繪,接著為該View設(shè)置標記位,然后把需要重繪的區(qū)域傳遞給父容器,即調(diào)用父容器的invalidateChild方法。
  • 做了一張圖


    invalidate()的原理.jpg

postInvalidate()的源碼解析

  • view.postInvalidate()繼承一個Textview,然后重寫方法,設(shè)置一個but,同時請求方法,打印日志:請求一次的輸出的結(jié)果
InvalidateTextView----------draw
InvalidateTextView------------onDraw
  • view.postInvalidate()詳情,由于方法是public,也可以調(diào)用一個時間,延遲多久開始執(zhí)行,這里是delayMilliseconds,毫秒
   public void postInvalidate() {
        postInvalidateDelayed(0);
    }
  • view.postInvalidateDelayed() 只有attachInfo不為null的時候才會繼續(xù)執(zhí)行,即只有確保視圖被添加到窗口的時候才會通知view樹重繪,因為這是一個異步方法,如果在視圖還未被添加到窗口就通知重繪的話會出現(xiàn)錯誤,所以這樣要做一下判斷!
   public void postInvalidateDelayed(long delayMilliseconds) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }
  • ViewRootImpl.dispatchInvalidateDelayed() 用了Handler,發(fā)送了一個異步消息到主線程,顯然這里發(fā)送的是MSG_INVALIDATE,即通知主線程刷新視圖
  /**
     * 用了Handler,發(fā)送了一個異步消息到主線程,顯然這里發(fā)送的是`MSG_INVALIDATE`,即通知主線程刷新視圖
      * @param view  只有 postInvalidate() 使用了handler 來發(fā)送消息
     * @param delayMilliseconds
     */
    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }
  • 通知對象去invalidate() ,底層也是調(diào)用的是invalidate(),只不過使用了mHandler發(fā)送消息,在這里就發(fā)送到主線程了,去調(diào)用invalidate()方法
      case MSG_INVALIDATE:
                    //通知對象去 invalidate ,底層也是調(diào)用的是 invalidate,只不過使用了handler發(fā)送消息
                    ((View) msg.obj).invalidate();
                    break;0
  • 方法的解釋 :postInvalidate是在非UI線程中調(diào)用,但是底層使用的是 invalidate(),通過ViewRootImpl的內(nèi)部handler: ViewRootHandler發(fā)送的消息,但是也可以在 主線程中使用,如果在強制在主線程中使用,內(nèi)部有個 handler 在工作,是不是顯得有點浪費 ,對吧!

  • postInvalidate()這個方法也可以主線程中使用

  • 做了一張圖


    postInvalidate()的原理.jpg
  • 最后說明幾點

    • invalidate()、postInvalidate()、requestLayout(),最底層處調(diào)用的是viewRootImpl.scheduleTraversals() 這個方法,requestLayout由于設(shè)置了measurelayout的標記位,所以requestLayout可以重新走一次繪制的流程
    • postInvalidate() 底層通過Handler把非UI線程的工作,調(diào)用的是invalidate().
    • invalidate()、requestLayout(),方法都檢查了是否在UI線程,不在的話,直接拋出異常,所以他們只能在UI線程中使用,postInvalidate()可以在UI線程和非UI線程中使用。
    • view自身不在適合某個區(qū)域,比如說``LayoutParams發(fā)生了改變,需要對其重新的測量、布局和繪制的三個流程,那么使用這個方法最合適requestLayout()`。
    • 如果說是在刷新當(dāng)前的view,是當(dāng)前的view進行重新的繪制,不會進行測量、布局流程。僅僅需要某個view重新繪制而不需要測量,使用invalidate()方法往往比requestLayout()方法更高效
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容