前言
Activity/Fragment/View 系列文章:
Android Activity 與View 的互動思考
Android Activity 生命周期詳解及監(jiān)聽
Android onSaveInstanceState/onRestoreInstanceState 原來要這么理解
Android Fragment 要你何用?
Android Activity/View/Window/Dialog/Fragment 深層次關(guān)聯(lián)(白話解析)
前幾天有個小伙伴問我個問題:當Activity 退到后臺(未銷毀),此時對View 進行requestLayout/invalidate 操作,會有效果嗎?雖然直覺和經(jīng)驗告訴我是沒有效果的,但是還是要以理服人。本篇循著Activity 生命周期,探索View 與其互動的細節(jié)。
通過本篇文章,你將了解到:
1、Activity 創(chuàng)建時如何關(guān)聯(lián)View
2、Activity 銷毀時如何解除關(guān)聯(lián)View
3、Activity 處在其它狀態(tài)時刷新View
1、Activity 創(chuàng)建時如何關(guān)聯(lián)View
Activity 生命周期

ViewTree 的創(chuàng)建
從一個最簡單的Android Hello World 說起:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
setContentView()指定一個布局文件,表示要在Activity上展示這個布局。
該方法有兩個主要作用:
1、將自定義的布局(View)加入到ViewTree里,而ViewTree的根就是DecorView。
2、將Window(PhoneWindow)和DecorView 關(guān)聯(lián)。
也就是說當Activity 處在"Create"狀態(tài)時,整個ViewTree已經(jīng)被創(chuàng)建了。
這個階段的調(diào)用流程如下:

其中1、2 表示執(zhí)行的順序,1先于2執(zhí)行。
可以看出,在onCreate調(diào)用之前,Activity 已經(jīng)創(chuàng)建了Window。而在setContentView()時,創(chuàng)建了ViewTree,并將Window與DecorView關(guān)聯(lián)上了。
將ViewTree 添加到Window
我們知道,Activity 處在"Create"狀態(tài)階段,頁面內(nèi)容是看不到的,需要等到"Resume"狀態(tài)才能看到,這是怎么一回事呢?

其中1、2 表示執(zhí)行的順序,1先于2執(zhí)行。
可以看出,先執(zhí)行了onResume,再執(zhí)行addView()操作。
提取部分代碼如下:
#ActivityThread.java
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
//最終調(diào)用到onResume
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
//通過 r.window 判斷,只有Activity 第一次啟動才會走這
if (r.window == null && !a.mFinished && willBeVisible) {
//取出Window賦值
r.window = r.activity.getWindow();
//取出DecorView
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//加入到Window里
wm.addView(decor, l);
} else {
...
}
}
} else if (!willBeVisible) {
...
}
...
}
分別將DecorView和WindowManager取出,將兩者進行關(guān)聯(lián),關(guān)聯(lián)的動作是WindowManager.addView()。
而addView()執(zhí)行的結(jié)果是將本次動作(測量、布局、繪制)提交到隊列里,等到有屏幕刷新信號過來時將會執(zhí)行隊列里的動作,最終將會從DecorView開始執(zhí)行VIew的三大流程(測量、布局、繪制),執(zhí)行完畢后我們將會看到頁面展示。
這也就是為什么很多文章經(jīng)常說的:頁面要到onResume()執(zhí)行后才會展示。
那么問題來了:在onResume()里能夠正常獲取布局的寬高嗎?
答案是:不能。因為onResume()和WindowManager.addView()執(zhí)行是在同一個線程里順序執(zhí)行的,此時addView()并沒有執(zhí)行。更進一步說,即使addView()執(zhí)行了,也只是將動作放到隊列里等待執(zhí)行而已。
有幾種方式可以在初次進入Activity時獲取到寬高:
1、在onResume()里post(Runnable),在Runnable里獲取寬高。
2、重寫View的onSizeChanged()方法,在該方法里獲取寬高。
3、監(jiān)聽View.addOnLayoutChangeListener()方法獲取寬高。
至此,隨著Activity從"Create"狀態(tài)到"Resume"狀態(tài),View也從創(chuàng)建到被添加到Window里,并最終展示在屏幕上。
2、Activity 銷毀時如何解除關(guān)聯(lián)View
眾所周知,Activity 銷毀的最后是執(zhí)行了onDestroy(),當Activity 處在"Destroy"狀態(tài)時,View是什么情況呢?

可以看出,先執(zhí)行了onDestroy(),再移除了View。
提取部分代碼如下:
#ActivityThread.java
public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges,
boolean getNonConfigInstance, String reason) {
//最終執(zhí)行到onDestroy
ActivityClientRecord r = performDestroyActivity(token, finishing,
configChanges, getNonConfigInstance, reason);
if (r != null) {
WindowManager wm = r.activity.getWindowManager();
View v = r.activity.mDecor;
if (v != null) {
if (r.activity.mWindowAdded) {
if (r.mPreserveWindow) {
r.window.clearContentView();
} else {
//移除View
wm.removeViewImmediate(v);
}
}
}
}
}
至此,隨著Activity 流轉(zhuǎn)到"Destroy"狀態(tài),View也被移除出了Window,此時頁面已經(jīng)不可見。
3、Activity 處在其它狀態(tài)時刷新View
上兩節(jié)闡述了Activity 創(chuàng)建與銷毀對應的View的操作,接下來分析創(chuàng)建與銷毀狀態(tài)的中間狀態(tài)是如何表現(xiàn)的。
分兩種情況:
1、Activity 處在"Resume"狀態(tài)時,對View進行刷新操作。
2、Activity 處在"Stop"狀態(tài)時,對View進行刷新操作。
注:此處的刷新指的是View.requestLayout()、View.invalidate()。
Resume 狀態(tài)下刷新View
要判斷刷新是否生效,只需要監(jiān)聽View的onMeasure()、onLayout()、onDraw()方法即可,它們?nèi)羰潜徽{(diào)用了,說明刷新操作成功了。
舉個簡單例子:
public class MyTextView extends AppCompatTextView {
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d("fish", "onMeasure called");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d("fish", "onLayout called");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d("fish", "onDraw called");
}
}
聲明一個類,繼承自AppCompatTextView,重寫onMeasure()/onLayout()/onDraw() 方法,并添加打印。
然后測試刷新操作,看打印結(jié)果:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flush_ui);
TextView textView = findViewById(R.id.tv);
findViewById(R.id.btn_request).setOnClickListener((v)->{
textView.requestLayout();
});
findViewById(R.id.btn_invalidate).setOnClickListener((v)->{
textView.invalidate();
});
}
毫無疑問,在"Resume"狀態(tài)下刷新View,當調(diào)用requestLayout()時,onMeasure()、onLayout()被執(zhí)行了;當調(diào)用invalidate()時,onDraw()被執(zhí)行了。
因此,頁面的刷新操作是成功的。
Stop 狀態(tài)下刷新View
改造測試Demo:
private Runnable requestRunnable = new Runnable() {
@Override
public void run() {
Log.d("fish", "request layout call");
textView.requestLayout();
textView.postDelayed(this, 1000);
}
};
不斷地延遲調(diào)用:
findViewById(R.id.btn_request).setOnClickListener((v)->{
textView.postDelayed(requestRunnable, 1000);
});
在Activity 處在"Resume"狀態(tài)時,"onMeasure called"一直在打印。
此時,回到桌面,Activity 處在"Stop"狀態(tài),"onMeasure called" 打印沒有了。
這說明:
當Activity 處在"Stop"狀態(tài)時,此時對View的刷新是無效的。
以上是針對requestLayout()的操作,實際上對于invalidate()效果亦是如此,就不重復演示了,可在文末的Demo鏈接里查看。
View 的刷新原理
View.requestLayout()
從實踐中驗證了猜想,接下來探究其原理。
之前在 Android invalidate/postInvalidate/requestLayout-徹底厘清 有分析過刷新原理,本次再來簡單回顧一下。
#View.java
public void requestLayout() {
...
//添加標記
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
//若是父布局沒有l(wèi)ayout,則會再次進行
//mParent 為父布局
mParent.requestLayout();
}
}
可以看出,一直調(diào)用父布局的requestLayout,調(diào)用的終點是:
#ViewRootImpl.java
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
此處就提交了刷新操作到隊列里,等待屏幕刷新信號的到來。

從實驗的結(jié)果來看,可以肯定的是requestLayout 請求沒有分發(fā)到ViewRootImpl,甚至大膽猜測TextView.reqeustLayout()請求沒有交給父布局。
而此處判斷的依據(jù)是:
mParent.isLayoutRequested()
該方法實現(xiàn)為:
public boolean isLayoutRequested() {
return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}
顯而易見,其實就是判斷標記位:PFLAG_FORCE_LAYOUT。
接著來尋找該標記位在哪里改變了。此處直接說結(jié)論,更詳細的分析請移步:
Android 自定義View之Measure過程
1、添加該標記的時機在View.requestLayout時。
2、清除該標記的時機是View.layout()時。
當View.layout 執(zhí)行后,說明View的擺放位置已經(jīng)確定,因此標記可以清空了。
添加標記和清除標記是成對出現(xiàn)的,requestLayout 沒有提交給父布局,說明PFLAG_FORCE_LAYOUT 只是添加了,沒有被清除,也就是說父布局的layout操作沒有執(zhí)行,當然它的measure操作也沒執(zhí)行
問題就轉(zhuǎn)到了:為什么父布局沒有執(zhí)行measure/layout?
尋根溯流,三大流程的發(fā)起是在ViewRootImpl實現(xiàn)的,重點方法:performTraversals()
而該方法里分別執(zhí)行了performMeasure、performLayout、performDraw。最終這些方法執(zhí)行到onMeasure、onLayout、onDraw 里。
執(zhí)行performMeasure 前提條件是:
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
執(zhí)行performLayout 前提條件是:
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
我們注意到了mStopped 變量,當mStopped=false的時候才會執(zhí)行performMeasure、performLayout。
只需要找到mStopped什么時候變?yōu)閠rue,答案就找到了。
#ViewRootImpl.java
void setWindowStopped(boolean stopped) {
checkThread();
//不一致才會執(zhí)行,此處會執(zhí)行兩次
if (mStopped != stopped) {
//修改mStopped
mStopped = stopped;
final ThreadedRenderer renderer = mAttachInfo.mThreadedRenderer;
if (renderer != null) {
renderer.setStopped(mStopped);
}
if (!mStopped) {
//如果不是停止,那么就是開始
mNewSurfaceNeeded = true;
//重新提交刷新動作到隊列里。
scheduleTraversals();
} else {
//釋放資源
if (renderer != null) {
renderer.destroyHardwareResources(mView);
}
}
...
if (mStopped) {
if (mSurfaceHolder != null && mSurface.isValid()) {
notifySurfaceDestroyed();
}
//銷毀surface
destroySurface();
}
}
}
1、當Activity 處在"Stop"狀態(tài)時,AMS 發(fā)出指令給ActivityThread,最終將會執(zhí)行到ViewRootImpl. setWindowStopped(boolean stopped),將成員變量mStopped置為false。
2、當要執(zhí)行View的三大流程時,發(fā)現(xiàn)mStopped==false,表示當前Activity 已經(jīng)處在"Stop"狀態(tài)了,因此不會執(zhí)行刷新操作了。
以上解釋了:
當Activity處在"Stop"狀態(tài)時,View.requestLayout()是沒有效果的原因。
View.invalidate()
與View.requestLayout 類似:
#View.java
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
//判斷標記
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;
}
...
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);
}
}
}
也是通過層層調(diào)用,最終到ViewRootImpl.java里的:
#ViewRootImpl.java
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
//提交到刷新隊列,等待屏幕信號的到來
scheduleTraversals();
}
}

當Activity 處在"Stop"狀態(tài)時,因為View的PFLAG_DRAWN標記沒有被添加,所以在invalidateInternal()方法里就不會再執(zhí)行p.invalidateChild(this, damage);
而PFLAG_DRAWN 標記是執(zhí)行了View.draw(x1,x2,x3)方法時添加的,表示這一次的繪制動作已經(jīng)完成。
與requestLayout 一樣,因為draw過程沒有被執(zhí)行,因此看看執(zhí)行draw過程的前置條件:
#ViewRootImpl.java
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
而決定isViewVisible的因素是:mAppVisible。
該變量被賦值的地方:
#ViewRootImpl.java
void handleAppVisibility(boolean visible) {
if (mAppVisible != visible) {
mAppVisible = visible;
mAppVisibilityChanged = true;
//提交刷新動作
scheduleTraversals();
if (!mAppVisible) {
WindowManagerGlobal.trimForeground();
}
}
}
當Activity 處在"Pause"狀態(tài)時,AMS 發(fā)出視圖可見性更改的命令,最終會執(zhí)行到ViewRootImp.handleAppVisibility(),此時mAppVisible==false,表示App已經(jīng)不可見。
而執(zhí)行perfromDraw()前置條件是App可見。
當Activity處在"Pause"、"Stop"狀態(tài)時,View.invalidate()是沒有效果的原因。
注意:此處的"Pause" 狀態(tài)應該排除其上層有透明非全屏的Activity,此種場景下是不會調(diào)用ViewRootImp.handleAppVisibility()
從Stop到Start/Resume View 是如何刷新的
從上面的分析可知,當Activity 變?yōu)镾top狀態(tài)時,顯示有關(guān)的Surface、Render都已經(jīng)被銷毀。當從Stop狀態(tài)回到Resume狀態(tài)時,這些又是怎么觸發(fā)的呢?
從ViewRootImpl.setWindowStopped()與ViewRootImpl.handleAppVisibility() 方法的實現(xiàn)可知:
1、在可見時ViewRootImpl.setWindowStopped()會調(diào)用scheduleTraversals()。
2、ViewRootImpl.handleAppVisibility() 則是每次調(diào)用都會觸發(fā)scheduleTraversals() 調(diào)用。
而scheduleTraversals()會觸發(fā)三大流程(Measure/Layout/Draw),這樣當我們App從后臺退到前臺時,界面就完成了渲染并展示了。
本文基于Android 10.0
Demo 地址:測試刷新
接下來將重點分析Activity/Fragment的深層次關(guān)聯(lián),以及整個生命周期的聯(lián)動,最后自然而然就會進入Jetpack分析。
您若喜歡,請點贊、關(guān)注,您的鼓勵是我前進的動力
持續(xù)更新中,和我一起步步為營系統(tǒng)、深入學習Android
1、Android各種Context的前世今生
2、Android DecorView 必知必會
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分發(fā)全套服務
6、Android invalidate/postInvalidate/requestLayout 徹底厘清
7、Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
8、Android事件驅(qū)動Handler-Message-Looper解析
9、Android 鍵盤一招搞定
10、Android 各種坐標徹底明了
11、Android Activity/Window/View 的background
12、Android Activity創(chuàng)建到View的顯示過
13、Android IPC 系列
14、Android 存儲系列
15、Java 并發(fā)系列不再疑惑
16、Java 線程池系列