Android Activity 与View 的互动思考¶
原文作者:小鱼人爱编程
前言¶
系列文章
前几天有个小伙伴问我个问题:当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);
}
- 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()操作。
提取部分代码如下:
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。
提取部分代码如下:
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);
}
}
}
}
}
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");
}
}
然后测试刷新操作,看打印结果:
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();
});
}
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 处在"Stop"状态,"onMeasure called" 打印没有了。 这说明:
当Activity 处在"Stop"状态时,此时对View的刷新是无效的。
以上是针对requestLayout()的操作,实际上对于invalidate()效果亦是如此,就不重复演示了,可在文末的Demo链接里查看。
View 的刷新原理¶
View.requestLayout()¶
从实践中验证了猜想,接下来探究其原理。 之前在 Android invalidate/postInvalidate/requestLayout-彻底厘清 有分析过刷新原理,本次再来简单回顾一下。
public void requestLayout() {
//...
//添加标记
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
//若是父布局没有layout,则会再次进行
//mParent 为父布局
mParent.requestLayout();
}
}
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
从实验的结果来看,可以肯定的是requestLayout 请求没有分发到ViewRootImpl,甚至大胆猜测TextView.reqeustLayout()请求没有交给父布局。 而此处判断的依据是:
该方法实现为:
public boolean isLayoutRequested() {
return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}
显而易见,其实就是判断标记位:PFLAG_FORCE_LAYOUT。
接着来寻找该标记位在哪里改变了。此处直接说结论,更详细的分析请移步:
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 前提条件是:
执行performLayout 前提条件是:
我们注意到了mStopped 变量,当mStopped=false的时候才会执行performMeasure、performLayout。
只需要找到mStopped什么时候变为true,答案就找到了。
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 类似:
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);
}
}
}
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过程的前置条件:
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
而决定isViewVisible的因素是:mAppVisible。
该变量被赋值的地方:
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分析。
本站说明
一起在知识的海洋里呛水吧。广告内容与本站无关。如果喜欢本站内容,欢迎投喂作者,谢谢支持服务器。如有疑问和建议,欢迎在下方评论~