前言
Activity/Fragment/View 系列文章:
Android Activity 与View 的互动思考
Android Activity 生命周期详解及监听
Android onSaveInstanceState/onRestoreInstanceState 原来要这么理解
Android Fragment 要你何用?
Android Activity/View/Window/Dialog/Fragment 深层次关联(白话解析)
前几天有个小伙伴问我个问题:当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()执行了,也只是将动作放到队列里等待执行而已。
有几种方式可以在初次进入Activity时获取到宽高:
1、在onResume()里post(Runnable),在Runnable里获取宽高。
2、重写View的onSizeChanged()方法,在该方法里获取宽高。
3、监听View.addOnLayoutChangeListener()方法获取宽高。
至此,随着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 处在"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()被执行了。
因此,页面的刷新操作是成功的。
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 处在"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 处在"Stop"状态时,AMS 发出指令给ActivityThread,最终将会执行到ViewRootImpl. setWindowStopped(boolean stopped),将成员变量mStopped置为false。
2、当要执行View的三大流程时,发现mStopped==false,表示当前Activity 已经处在"Stop"状态了,因此不会执行刷新操作了。
以上解释了:
当Activity处在"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 处在"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()是没有效果的原因。
注意:此处的"Pause" 状态应该排除其上层有透明非全屏的Activity,此种场景下是不会调用ViewRootImp.handleAppVisibility()
从Stop到Start/Resume View 是如何刷新的
从上面的分析可知,当Activity 变为Stop状态时,显示有关的Surface、Render都已经被销毁。当从Stop状态回到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
1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列