前言
系列文章:
Android Activity創建到View的顯示過程
Android Activity 與View 的互動思考
Android invalidate/postInvalidate/requestLayout-徹底厘清
Android 容易遺漏的刷新小細節
前幾篇分析了Measure、Layout、Draw 過程,這三個過程在第一次展示View的時候都會調用。那之后更改了View的屬性呢?比如更改顏色、更換文字內容、更換圖片等,還會走這三個過程嗎?循著這個思路,來分析Invalidate/RequestLayout流程。
通過本篇文章,你將了解到:
1、Invalidate 流程
2、RequestLayout 流程
3、Invalidate/RequestLayout 使用場合
4、子線程真不能繪制UI嗎
5、postInvalidate 流程
Invalidate 流程
一個小Demo
public class MyView extends View {
private Paint paint;
private @ColorInt int color = Color.RED;
public MyView(Context context) {
super(context);
init();
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
paint = new Paint();
paint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
//涂紅色
canvas.drawColor(color);
}
public void setColor(@ColorInt int color) {
this.color = color;
invalidate();
}
}
MyView 默認展示一塊紅色的矩形區域,暴露給外界的方法:setColor
用以改變繪制的顏色。顏色改變后,需要重新執行onDraw(xx)才能看到改變后的效果,通過invalidate()方法觸發onDraw(xx)調用。
接下來看看invalidate()方法是怎么觸發onDraw(xx)方法執行的。
invalidate() 調用棧
invalidate顧名思義:使某個東西無效。在這里表示使當前繪制內容無效,需要重新繪制。當然,一般來說常常簡單稱作:刷新。
invalidate()是View.java 里的方法。
#View.java
public void invalidate() {
invalidate(true);
}
public void invalidate(boolean invalidateCache) {
//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 (skipInvalidate()) {
return;
}
//PFLAG_DRAWN 表示此前該View已經繪制過 PFLAG_HAS_BOUNDS表示該View已經layout過,確定過坐標了
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) {
//默認true
mLastIsOpaque = isOpaque();
//清除繪制標記
mPrivateFlags &= ~PFLAG_DRAWN;
}
//需要繪制
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
//1、加上繪制失效標記
//2、清除繪制緩存有效標記
//這兩標記在硬件加速繪制分支用到
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
//記錄需要重新繪制的區域 damge,該區域為該View尺寸
damage.set(l, t, r, b);
//p 為該View的父布局
//調用父布局的invalidateChild
p.invalidateChild(this, damage);
}
...
}
}
從上可知,當前要刷新的View確定了刷新區域后即調用了父布局的invalidateChild(xx)方法。該方法為ViewGroup里的final方法。
#ViewGroup.java
public final void invalidateChild(View child, final Rect dirty) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null && attachInfo.mHardwareAccelerated) {
//1、如果是支持硬件加速,則走該分支
onDescendantInvalidated(child, child);
return;
}
//2、軟件繪制
ViewParent parent = this;
if (attachInfo != null) {
//動畫相關,忽略
...
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
...
parent = parent.invalidateChildInParent(location, dirty);
//動畫相關
} while (parent != null);
}
}
由上可知,在該方法里區分了硬件加速繪制與軟件繪制,分別來看看兩者區別:
硬件加速繪制分支
如果該Window支持硬件加速,則走下邊流程:
#ViewGroup.java
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);
if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {
//此處都會走
mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;
//清除繪制緩存有效標記
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
if (mLayerType == LAYER_TYPE_SOFTWARE) {
//如果是開啟了軟件繪制,則加上繪制失效標記
mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
//更改target指向
target = this;
}
if (mParent != null) {
//調用父布局的onDescendantInvalidated
mParent.onDescendantInvalidated(this, target);
}
}
onDescendantInvalidated 方法的目的是不斷向上尋找其父布局,并將父布局PFLAG_DRAWING_CACHE_VALID 標記清空,也就是繪制緩存清空。
而我們知道,根View的mParent指向ViewRootImpl對象,因此來看看它里面的onDescendantInvalidated()方法:
#ViewRootImpl.java
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
// TODO: Re-enable after camera is fixed or consider targetSdk checking this
// checkThread();
if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
mIsAnimating = true;
}
invalidate();
}
@UnsupportedAppUsage
void invalidate() {
//mDirty 為臟區域,也就是需要重繪的區域
//mWidth,mHeight 為Window尺寸
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
//開啟View 三大流程
scheduleTraversals();
}
}
做個小結:
1、invalidate() 對于支持硬件加速來說,目的就是尋找需要重繪的View。當前View肯定是需要重繪的,繼續遞歸尋找其父布局直至到根View。
2、如果該View需要重繪,則加上PFLAG_INVALIDATED 標記。
3、設置重繪區域。
用圖表示硬件加速繪制的invaldiate流程:
軟件繪制分支
如果該Window不支持硬件加速,那么走軟件繪制分支:
parent.invalidateChildInParent(location, dirty) 返回mParent,只要mParent不為空那么一直調用invalidateChildInParent(xx),實際上這也是遍歷ViewTree過程,來看看關鍵invalidateChildInParent(xx):
#ViewGroup.java
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
//dirty 為失效的區域,也就是需要重繪的區域
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
//該View繪制過或者繪制緩存有效
if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
!= FLAG_OPTIMIZE_INVALIDATE) {
//修正重繪的區域
dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
location[CHILD_TOP_INDEX] - mScrollY);
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
//如果允許子布局超過父布局區域展示
//則該dirty 區域需要擴大
dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
}
final int left = mLeft;
final int top = mTop;
if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
//默認會走這
//如果不允許子布局超過父布局區域展示,則取相交區域
if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
dirty.setEmpty();
}
}
//記錄偏移,用以不斷修正重繪區域,使之相對計算出相對屏幕的坐標
location[CHILD_LEFT_INDEX] = left;
location[CHILD_TOP_INDEX] = top;
} else {
...
}
//標記緩存失效
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
if (mLayerType != LAYER_TYPE_NONE) {
//如果設置了緩存類型,則標記該View需要重繪
mPrivateFlags |= PFLAG_INVALIDATED;
}
//返回父布局
return mParent;
}
return null;
}
與硬件加速繪制一致,最終調用ViewRootImpl invalidateChildInParent(xx),來看看實現:
#ViewRootImpl.java
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
if (dirty == null) {
//臟區域為空,則默認刷新整個窗口
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
...
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
final Rect localDirty = mDirty;
//合并臟區域,取并集
localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
...
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
//開啟View的三大繪制流程
scheduleTraversals();
}
}
做個小結:
1、invalidate() 對于軟件繪制來說,目的就是尋找需要重繪的區域。
2、確定重繪的區域在Window里的位置,該區域需要重新繪制。
用圖表示軟件繪制invalidate流程:
上述分析了硬件加速繪制與軟件繪制時invalidate的不同,它們的最終目的都是為了重走Draw過程。重走Draw過程通過調用scheduleTraversals() 觸發的,來看看是如何觸發的。
想了解更多硬件加速繪制請移步:
Android 自定義View之Draw過程(中)
觸發Draw過程
scheduleTraversals 詳細分析在這篇文章:
Android Activity創建到View的顯示過程
三大流程真正開啟在ViewRootImpl->performTraversals(),在該方法里根據一定的條件執行了Measure(測量)、Layout(擺放)、Draw(繪制)。
本次著重分析如何觸發Draw過程。
#ViewRootImpl.java
private void performDraw() {
...
try {
//調用draw 方法
boolean canUseAsync = draw(fullRedrawNeeded);
...
} finally {
mIsDrawing = false;
}
...
}
private boolean draw(boolean fullRedrawNeeded) {
//mSurface 在ViewRootImpl 構建的時候創建
Surface surface = mSurface;
if (!surface.isValid()) {
return false;
}
...
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
//invalidate 時,dirty就已經被賦值
//滿足其中一個條件即可,重點關注第一個條件
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
...
//硬件加速繪制
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
...
//軟件繪制
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}
...
return useAsyncReport;
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
final Canvas canvas;
try {
//dirty 為需要繪制的區域
//invalidate 指定的刷新區域會影響canvas繪制區域
canvas = mSurface.lockCanvas(dirty);
} catch (Surface.OutOfResourcesException e) {
return false;
} catch (IllegalArgumentException e) {
return false;
} finally {
}
try {
//ViewTree 開始繪制
mView.draw(canvas);
} finally {
..
}
return true;
}
可以看出,invalidate 最終觸發了Draw過程。
1、不管是硬件加速繪制還是軟件繪制,都會設置重繪的矩形區域。對于硬件加速繪制來說,重繪的區域為整個Window的大小。而對于軟件繪制則是設置相交的矩形區域。
2、只要重繪區域不為空,那么當開啟三大流程時,Draw過程必然被調用。
3、對于硬件加速繪制來說,通過繪制標記控制需要重繪的View,因此當我們調用view.invalidate()時,該view被設置了重繪標記,在Draw過程里該view draw(xx)被調用。當然如果其父布局設置了軟件緩存,則其父布局也需要被重繪,父布局下的子布局也需要重繪。
4、對于軟件繪制來說,整個ViewTree的Draw過程都會被調用,只是Canvas僅僅繪制重繪區域指定的矩形區域。
可以看出,啟用硬件加速繪制可以避免不必要的繪制。
關于硬件加速繪制與軟件繪制詳細區別,請移步系列文章:
Android 自定義View之Draw過程(上)
最后,用圖表示invalidate流程:
RequestLayout 流程
顧名思義,重新請求布局。
來看看View.requestLayout()方法:
#View.java
public void requestLayout() {
//清空測量緩存
if (mMeasureCache != null) mMeasureCache.clear();
...
//添加強制layout 標記,該標記觸發layout
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
//添加重繪標記
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
//如果上次的layout 請求已經完成
//父布局繼續調用requestLayout
mParent.requestLayout();
}
...
}
可以看出,這個遞歸調用和invalidate一樣的套路,向上尋找其父布局,一直到ViewRootImpl為止,給每個布局設置PFLAG_FORCE_LAYOUT和PFLAG_INVALIDATED標記。
查看ViewRootImpl requestLayout()
#ViewRootImpl.java
public void requestLayout() {
//是否正在進行layout過程
if (!mHandlingLayoutInLayoutRequest) {
//檢查線程是否一致
checkThread();
//標記有一次layout的請求
mLayoutRequested = true;
//開啟View 三大流程
scheduleTraversals();
}
}
很明顯,requestLayout目的很單純:
1、向上尋找父布局、并設置強制layout標記
2、最終開啟三大繪制流程
和invalidate一樣的配方,當刷新信號來到之時,調用doTraversal()->performTraversals(),而在performTraversals()里真正執行三大流程。
#ViewRootImpl.java
private void performTraversals() {
//mLayoutRequested 在requestLayout時賦值為true
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
//measure 過程
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
...
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
//layout 過程
performLayout(lp, mWidth, mHeight);
}
...
}
由此可見:
1、requestLayout 最終將會觸發Measure、Layout 過程。
2、由于沒有設置重繪區域,因此Draw 過程將不會觸發。
之前設置的PFLAG_FORCE_LAYOUT標記有啥用呢?
回憶一下measure 過程:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
//requestLayout時,PFLAG_FORCE_LAYOUT 標記被設置
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
...
if (forceLayout || needsLayout) {
...
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//測量
onMeasure(widthMeasureSpec, heightMeasureSpec);
} else {
...
}
...
}
}
}
PFLAG_FORCE_LAYOUT 標記打上之后,會觸發onMeasure()測量自身及其子布局。
試想一下,假設View的尺寸改變了,變大了,那么調用了requestLayout后因為走了Measure、Layout 過程,測量、擺放倒是重新設置了,但是不調用Draw出不來效果啊。實際上,View layout時候已經考慮到了。
在View.layout(xx)->setFrame(xx)里
#View.java
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
...
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
//尺寸發生改變 調用invalidate 傳入true,否則傳入false
invalidate(sizeChanged);
...
}
...
return changed;
}
也就是說:
1、requestLayout調用后,可能會觸發invalidate。
2、若是觸發了invalidate(),不管傳入true還是false,都會走重繪流程。
關于measure、layout 過程更深入的分析,請移步:
用圖表示requestLayout過程:
Invalidate/RequestLayout 使用場合
結合requestLayout和invalidate與View三大流程關系,有如下圖:
總結一下:
1、invalidate調用后只會觸發Draw 過程。
2、requestLayout 會觸發Measure、Layout過程,如果尺寸發生改變,則會調用invalidate。
3、當涉及View的尺寸、位置變化時使用requestLayout。
4、當僅僅需要重繪時調用invalidate。
5、如果不確定requestLayout 是否觸發invalidate,可在requestLayout后繼續調用invalidate。
上面僅僅說明了單個布局Invalidate/RequestLayout聯系,那么如果父布局調用了invalidate,那么子布局會走重繪過程嗎?接下來列舉這些關系。
子布局/父布局 Invalidate/RequestLayout 關系
子布局Invalidate
如果是軟件繪制或者父布局開啟了軟件緩存繪制,父布局會走重繪過程(前提是WILL_NOT_DRAW標記沒設置)。
子布局RequestLayout
父布局會重走Measure、Layout過程。
父布局Invalidate
如果是軟件繪制,則子布局會走重繪過程。
父布局RequestLayout
如果父布局尺寸發生了改變,則會觸發子布局Measure過程、Layout過程。
子線程真不能繪制UI嗎
在Activity onCreate里創建子線程并展示對話框:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_group);
new Thread(new Runnable() {
@Override
public void run() {
TextView textView = new TextView(MainActivity.this);
textView.setText("hello thread");
Looper.prepare();
Dialog dialog = new Dialog(MainActivity.this);
dialog.setContentView(textView);
dialog.show();
Looper.loop();
}
}).start();
}
答案是可以的,接下來分析為什么可以。
在分析ViewRootImpl里requestLayout/invalidate過程中,發現其內部調用了checkThread()方法:
#ViewRootImpl.java
void checkThread() {
//當前調用線程與mThread不是同一線程則會拋出異常
if (mThread != Thread.currentThread()) {
//簡單翻譯來說:只有創建了ViewTree的線程才能操作里邊的View
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
問題的關鍵是mThread是什么?從哪里來?
#ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {
...
//mThread 為Thread類型
//也就是說哪個線程執行了構造ViewRootImpl對象,那么mThread就是指向那個線程
mThread = Thread.currentThread();
...
}
而創建ViewRootImpl對象是在調用WindowManager.addView(xx)過程中創建的。
關于WindowManager/Window 請移步:Window/WindowManager 不可不知之事
現在回過頭來看Dialog創建就比較明朗了:
1、dialog.show() 調用WindowManager.addView(xx),此時是子線程調用,因此ViewRootImpl對象是在子線程調用的,進而mThread指向子線程。
2、當ViewRootImpl對象構建成功后,調用其setView(xx)方法,里面調用了requestLayout,此時還是子線程。
3、checkThread()判斷是同一線程,因此不會拋出異常。
實際上,"子線程不能更新ui" 更合理的表述應為:View只能被構建了ViewTree的線程操作。只是通常來說,Activity 構建ViewTree的線程被稱作UI(主)線程,因此才會有上述說法。
postInvalidate 流程
既然invalidate()只能主線程調用(硬件加速條件下,不調用checkThread()),那如果想在子線程調用呢?當然想到的是先通過Handler切換到主線程,再執行invalidate(),但是每次這么寫有點冗余,幸好,View里提供了postInvalidate:
#View.java
public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
//還是靠ViewRootImpl
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
切到ViewRootImpl.java
#ViewRootImpl.java
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
//此處Message.obj = view
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
//obj 即為待刷新的View
((View) msg.obj).invalidate();
break;
...
}
}
發現了真相:
postInvalidate 通過ViewRootImpl 里的handler切換到UI線程,最終執行
invalidate()。
ViewRootImpl 里的hanlder綁定的線程即是UI線程。
本文基于Android 10.0