前言
前几天有个小伙伴问我个问题:当Activity 退到后台(未销毁),此时对View 进行requestLayout/invalidate 操作,会有效果吗?虽然直觉和经验告诉我是没有效果的,但是还是要以理服人。本篇循着Activity 生命周期,探索View 与其互动的细节。 通过本篇文章,你将了解到:
1、Activity 创建时如何关联View 2、Activity 销毁时如何解除关联View 3、Activity 处在其它状态时刷新View
1、Activity 创建时如何关联View
Activity 生命周期
ViewTree 的创建
从一个最简单的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 关联。
也就是说当Activity 处在"Create"状态时,整个ViewTree已经被创建了。 这个阶段的调用流程如下: 其中1、2 表示执行的顺序,1先于2执行。
可以看出,在onCreate调用之前,Activity 已经创建了Window。而在setContentView()时,创建了ViewTree,并将Window与DecorView关联上了。
将ViewTree 添加到Window
我们知道,Activity 处在"Create"状态阶段,页面内容是看不到的,需要等到"Resume"状态才能看到,这是怎么一回事呢? 其中1、2 表示执行的顺序,1先于2执行。 可以看出,先执行了onResume,再执行addView()操作。 提取部分代码如下:
#ActivityThread.java
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
//最终调用到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取出,将两者进行关联,关联的动作是WindowManager.addView()。 而addView()执行的结果是将本次动作(测量、布局、绘制)提交到队列里,等到有屏幕刷新信号过来时将会执行队列里的动作,最终将会从DecorView开始执行VIew的三大流程(测量、布局、绘制),执行完毕后我们将会看到页面展示。
这也就是为什么很多文章经常说的:页面要到onResume()执行后才会展示。 那么问题来了:在onResume()里能够正常获取布局的宽高吗? 答案是:不能。因为onResume()和WindowManager.addView()执行是在同一个线程里顺序执行的,此时addView()并没有执行。更进一步说,即使addView()执行了,也只是将动作放到队列里等待执行而已。 有两种方式可以获取到宽高:
1、在onResume()里post(Runnable),在Runnable里获取宽高。 2、重写View的onSizeChanged()方法,在该方法里获取宽高。
至此,随着Activity从"Create"状态到"Resume"状态,View也从创建到被添加到Window里,并最终展示在屏幕上。
2、Activity 销毁时如何解除关联View
众所周知,Activity 销毁的最后是执行了onDestroy(),当Activity 处在"Destroy"状态时,View是什么情况呢?
可以看出,先执行了onDestroy(),再移除了View。 提取部分代码如下:
#ActivityThread.java
public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges,
boolean getNonConfigInstance, String reason) {
//最终执行到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 流转到"Destroy"状态,View也被移除出了Window,此时页面已经不可见。
3、Activity 处在其它状态时刷新View
上两节阐述了Activity 创建与销毁对应的View的操作,接下来分析创建与销毁状态的中间状态是如何表现的。 分两种情况:
1、Activity 处在"Resume"状态时,对View进行刷新操作。 2、Activity 处在"Pause"或者"Stop"状态时,对View进行刷新操作。
注:此处的刷新指的是View.requestLayout()、View.invalidate()。
Resume 状态下刷新View
要判断刷新是否生效,只需要监听View的onMeasure()、onLayout()、onDraw()方法即可,它们若是被调用了,说明刷新操作成功了。 举个简单例子:
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() 方法,并添加打印。
然后测试刷新操作,看打印结果:
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"状态下刷新View,当调用requestLayout()时,onMeasure()、onLayout()被执行了;当调用invalidate()时,onDraw()被执行了。 因此,页面的刷新操作是成功的。
Pause/Stop 状态下刷新View
改造测试Demo:
private Runnable requestRunnable = new Runnable() {
@Override
public void run() {
Log.d("fish", "request layout call");
textView.requestLayout();
textView.postDelayed(this, 1000);
}
};
不断地延迟调用:
findViewById(R.id.btn_request).setOnClickListener((v)->{
textView.postDelayed(requestRunnable, 1000);
});
在Activity 处在"Resume"状态时,"onMeasure called"一直在打印。 此时,回到桌面,Activity 处在"Stop"状态,“onMeasure called” 打印没有了。 这说明:
当Activity 处在"Pause"、"Stop"状态时,此时对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()) {
//若是父布局没有layout,则会再次进行
//mParent 为父布局
mParent.requestLayout();
}
}
可以看出,一直调用父布局的requestLayout,调用的终点是:
#ViewRootImpl.java
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
此处就提交了刷新操作到队列里,等待屏幕刷新信号的到来。
从实验的结果来看,可以肯定的是requestLayout 请求没有分发到ViewRootImpl,甚至大胆猜测TextView.reqeustLayout()请求没有交给父布局。 而此处判断的依据是:
mParent.isLayoutRequested()
该方法实现为:
public boolean isLayoutRequested() {
return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}
显而易见,其实就是判断标记位:PFLAG_FORCE_LAYOUT。 接着来寻找该标记位在哪里改变了。此处直接说结论,更详细的分析请移步: Android 自定义View之Measure过程
1、添加该标记的时机在View.requestLayout时。 2、清除该标记的时机是View.layout()时。
当View.layout 执行后,说明View的摆放位置已经确定,因此标记可以清空了。 添加标记和清除标记是成对出现的,requestLayout 没有提交给父布局,说明PFLAG_FORCE_LAYOUT 只是添加了,没有被清除,也就是说父布局的layout操作没有执行,当然它的measure操作也没执行
问题就转到了:为什么父布局没有执行measure/layout? 寻根溯流,三大流程的发起是在ViewRootImpl实现的,重点方法:performTraversals() 而该方法里分别执行了performMeasure、performLayout、performDraw。最终这些方法执行到onMeasure、onLayout、onDraw 里。 执行performMeasure 前提条件是:
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
执行performLayout 前提条件是:
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
我们注意到了mStopped 变量,当mStopped=false的时候才会执行performMeasure、performLayout。
只需要找到mStopped什么时候变为true,答案就找到了。
#ViewRootImpl.java
void setWindowStopped(boolean stopped) {
checkThread();
//不一致才会执行,此处会执行两次
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 处在"Pause"状态时,AMS 发出指令给ActivityThread,最终将会执行到ViewRootImpl. setWindowStopped(boolean stopped),将成员变量mStopped置为false。 2、当要执行View的三大流程时,发现mStopped==false,表示当前Activity 已经处在"Pause"状态了,因此不会执行刷新操作了。
以上解释了:
当Activity处在"Pause"、"Stop"状态时,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);
//调用父布局
p.invalidateChild(this, damage);
}
}
}
也是通过层层调用,最终到ViewRootImpl.java里的:
#ViewRootImpl.java
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
//提交到刷新队列,等待屏幕信号的到来
scheduleTraversals();
}
}
当Activity 处在"Pause"、"Stop"状态时,因为View的PFLAG_DRAWN标记没有被添加,所以在invalidateInternal()方法里就不会再执行p.invalidateChild(this, damage);
而PFLAG_DRAWN 标记是执行了View.draw(x1,x2,x3)方法时添加的,表示这一次的绘制动作已经完成。 与requestLayout 一样,因为draw过程没有被执行,因此看看执行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"状态时,AMS 发出视图可见性更改的命令,最终会执行到ViewRootImp.handleAppVisibility(),此时mAppVisible==false,表示App已经不可见。 而执行perfromDraw()前置条件是App可见。
当Activity处在"Pause"、"Stop"状态时,View.invalidate()是没有效果的原因。
从Stop/Pause到Start/Resume View 是如何刷新的
从上面的分析可知,当Activity 变为Pause状态时,显示有关的Surface、Render都已经被销毁。当从Pause状态回到Resume状态时,这些又是怎么触发的呢?
从ViewRootImpl.setWindowStopped()与ViewRootImpl.handleAppVisibility() 方法的实现可知:
1、在可见时ViewRootImpl.setWindowStopped()会调用scheduleTraversals()。 2、ViewRootImpl.handleAppVisibility() 则是每次调用都会触发scheduleTraversals() 调用。 而scheduleTraversals()会触发三大流程(Measure/Layout/Draw),这样当我们App从后台退到前台时,界面就完成了渲染并展示了。
本文基于Android 10.0 Demo 地址:测试刷新
接下来将重点分析Activity/Fragment的深层次关联,以及整个生命周期的联动,最后自然而然就会进入Jetpack分析。
您若喜欢,请点赞、关注,您的鼓励是我前进的动力
持续更新中,和我一起步步为营系统、深入学习Android
|