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