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

來源:互聯網
上載者:User
  • 最近在擼Golang有點上火了,來整理下源碼資料
  • 分析結果基於Audroid API 26

requestLayout()源碼分析

  • 假如在一個頁面上有個按鈕,點擊按鈕就對一個 view.requestLayout(),這個 view 執行的方法如下:
InvalidateTextView------onMeasureInvalidateTextView------onMeasureInvalidateTextView-------layoutInvalidateTextView--------onLayoutInvalidateTextView----------drawInvalidateTextView------------onDraw
  • view.requestLayout() 方法的詳情
  @CallSuper    public void requestLayout() {        // 清除繪製的緩衝        if (mMeasureCache != null) mMeasureCache.clear();        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {            //只有在布局邏輯中觸發請求,如果這是請求它的視圖,而不是其父階層中的視圖            ViewRootImpl viewRoot = getViewRootImpl();            //如果連續請求兩次,其中一次自動返回!            if (viewRoot != null && viewRoot.isInLayout()) {                if (!viewRoot.requestLayoutDuringLayout(this)) {                    return;                }            }            mAttachInfo.mViewRequestingLayout = this;        }       //todo   為當前view設定標記位 PFLAG_FORCE_LAYOUT        mPrivateFlags |= PFLAG_FORCE_LAYOUT;        mPrivateFlags |= PFLAG_INVALIDATED;        if (mParent != null && !mParent.isLayoutRequested()) {           //   todo  向父容器請求布局 這裡是向父容器請求布局,即調用父容器的requestLayout方法,為父容器添加PFLAG_FORCE_LAYOUT標記位,而父容器又會調用它的父容器的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,如果是,就返回,也就可以理解為: 如果連續請求兩次,並且其中的一次正在layout中,其中一次返回!這樣做是節約效能
   if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {            //只有在布局邏輯中觸發請求,如果這是請求它的視圖,而不是其父階層中的視圖            ViewRootImpl viewRoot = getViewRootImpl();            //如果連續請求兩次,其中一次自動返回!            if (viewRoot != null && viewRoot.isInLayout()) {                if (!viewRoot.requestLayoutDuringLayout(this)) {                    return;                }            }            mAttachInfo.mViewRequestingLayout = this;        }
  • 3、 為當前view設定標記位PFLAG_FORCE_LAYOUT,關於 |=符號:a|=b的意思就是把a和b按位或然後賦值給a 按位或的意思就是先把a和b都換成2進位,然後用或操作,相當於a=a|b
   mPrivateFlags |= PFLAG_FORCE_LAYOUT;   mPrivateFlags |= PFLAG_INVALIDATED;
  • 4、向父容器請求布局,即調用ViewGroup父容器的requestLayout()方法,為父容器添加PFLAG_FORCE_LAYOUT標記位,而父容器又會調用它的父容器的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()  會調用這個方法 void scheduleTraversals() {    if (!mTraversalScheduled) {        mTraversalScheduled = true;        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();        // 最終調用的是這個方法        mChoreographer.postCallback(                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);        if (!mUnbufferedInputDispatch) {            scheduleConsumeBatchedInput();        }        notifyRendererOfFramePending();        pokeDrawLockIfNeeded();    }}
  • 有個問題,我先拋出結論,requessLayout() 、invalidate()、postInvalidate()最終的底層調用的是 ViewRootImpl.scheduleTraversals()的方法,為什麼僅僅requessLayout()才會執行onMeasure() onLayout() onDraw()這幾個方法?

  • 關於view.measure()方法:在前面我們知道 mPrivateFlags |= PFLAG_FORCE_LAYOUT 所以 forceLayout = true,也就是會執行onMeasure(widthMeasureSpec, heightMeasureSpec);,同時執行完了以後呢,最後為標記位設定為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;            }             // 接著最後為標記位設定為PFLAG_LAYOUT_REQUIRED            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;        }       ...    }
  • 關於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) {            //第二次調用這個方法,,,            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()的分析的結果:view調用了這個方法,其實會從view樹重新進行一次測量、布局、繪製這三個流程。
  • 做了一張圖


    requestLayout()的原理.jpg

invalidate()源碼分析

  • view.invalidate();繼承一個Textview,然後重寫方法,設定一個but,同時要求方法,列印日誌:請求一次的輸出的結果
InvalidateTextView----------drawInvalidateTextView------------onDraw
  • 方法詳情 : view.invalidate()
  public void invalidate() {        invalidate(true);    }
  • 該視圖的繪圖緩衝是否也應無效。對於完全無效,設定為true,但是如果視圖的內容或維度沒有改變,則可以設定為false。
   public void invalidate(boolean invalidateCache) {        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);    }
  • invalidateInternal()方法詳情:其實關鍵的方法就是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;     }//根據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;         }         //設定PFLAG_DIRTY標記位         mPrivateFlags |= PFLAG_DIRTY;         if (invalidateCache) {             mPrivateFlags |= PFLAG_INVALIDATED;             mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;         }         //把需要重繪的地區傳遞給父容器         // 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);             //調用父容器的方法,向上傳遞事件             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、需要重新繪製的地區傳遞給父容器,向上傳遞事件,記住在這裡 damage這個變數肯定不為null,要不然在這個方法裡面就會直接拋出null 指標異常。
    p.invalidateChild(this, damage);
    • 4、損壞整個投影接收機,如果不需要。mBackground.isProjected(): 這張畫是否需要投影。
     if (mBackground != null && mBackground.isProjected()) {           final View receiver = getProjectionReceiver();           if (receiver != null) {               receiver.damageInParent();           }       }
  • 關鍵的方法:ViewRootImpl.invalidateChild(this, damage);
  @Override    public void invalidateChild(View child, Rect dirty) {        invalidateChildInParent(null, dirty);    }
  • invalidateChildInParent(null, dirty);進行了offset和union對座標的調整,然後把dirty地區的資訊儲存在mDirty中,最後調用了scheduleTraversals()方法,觸發View的工作流程,由於沒有添加measure和layout的標記位,因此measure、layout流程不會執行,而是直接從draw流程開始.

    @Overridepublic 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中的座標轉化為父容器中的座標,考慮mScrollX和mScrollY的影響         dirty.offset(0, -mCurScrollY);     }     if (mTranslator != null) {         mTranslator.translateRectInAppWindowToScreen(dirty);     }     if (mAttachInfo.mScalingRequired) {         dirty.inset(-1, -1);     }  }  //進行了offset和union對座標的調整  invalidateRectOnScreen(dirty);  return null;}
    • 1、檢查線程,不是ui線程,直接拋出異常.和requestLayout()一樣的
       checkThread();
    • 2、如果是從 invalidate() 方法過來的話,那麼dirty 肯定不為null 因為要是為null的話,前面調用方法的地方就拋出了null 指標的異常
     if (dirty == null) {     invalidate();     return null; }
    • 3、通過將dx=0添加到其左、右座標,並將 mCurScrollY 添加到其頂部和底部座標來抵消矩形。
     dirty.offset(0, -mCurScrollY);
    • 4、進行了offset和union對座標的調整
    invalidateRectOnScreen(dirty);
  • 關於invalidateRectOnScreen(dirty)方法:最終的關鍵的方法: 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 給當前的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();        }    }
  • 最終的關鍵的方法:ViewRootImpl.scheduleTraversals();,也就是會調用到這個對象mTraversalRunnable;也就是和requessLaout()最終調用的底層的方法一樣,只不過對於invalidate()沒有添加measure()layout()的標記位,後面的流程也就不會執行!具體的流程可以看這篇文章Android源碼分析(View的繪製流程)
  • 該方法的調用會引起View樹的重繪,常用於內部調用(比如 setVisiblity())或者需要重新整理介面的時候,需要在主線程(即UI線程)中調用該方法,invalidate有多個重載方法,但最終都會調用invalidateInternal方法,在這個方法內部,進行了一系列的判斷,判斷View是否需要重繪,接著為該View設定標記位,然後把需要重繪的地區傳遞給父容器,即調用父容器的invalidateChild方法。
  • 做了一張圖


    invalidate()的原理.jpg

postInvalidate()的源碼解析

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

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

  • 做了一張圖


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

    • invalidate()、postInvalidate()、requestLayout(),最底層處調用的是viewRootImpl.scheduleTraversals() 這個方法,requestLayout由於設定了measurelayout的標記位,所以requestLayout可以重新走一次繪製的流程
    • postInvalidate() 底層通過Handler把非UI線程的工作,調用的是invalidate().
    • invalidate()、requestLayout(),方法都檢查了是否在UI線程,不在的話,直接拋出異常,所以他們只能在UI線程中使用,postInvalidate()可以在UI線程和非UI線程中使用。
    • view自身不在適合某個地區,比如說``LayoutParams發生了改變,需要對其重新的測量、布局和繪製的三個流程,那麼使用這個方法最合適requestLayout()`。
    • 如果說是在重新整理當前的view,是當前的view進行重新的繪製,不會進行測量、布局流程。僅僅需要某個view重新繪製而不需要測量,使用invalidate()方法往往比requestLayout()方法更高效
相關關鍵詞:
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.