看到这个标题,你可能会想Android系统是否执行应用页面渲染,怎么会和页面的可见状态有关。明明每次异步请求结束之后,无论页面是否可见,界面元素都能正常显示。要回答这个问题,我们还是要回到Android源码里面去寻找线索。
我们知道Android应用页面渲染要依次经历measure、layout、draw三个过程,而这三个过程的触发者正是ViewRootImpl。ViewRootImpl接收Choreographer的固定频率同步信号,执行View树遍历操作performTraversals,measure、layout和draw正是在这里触发的。
// 本文所引用的代码节选自android-27源码
private void performTraversals() {
......
if (!mStopped || mReportNextDraw) {
......
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
}
......
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
......
}
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
......
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
......
performDraw();
}
......
}
从performTraversals的执行过程可以看到,performMeasure、performLayout以及dispatchOnGlobalLayout的执行与否与mStopped这个变量的取值密切相关,而performDraw仅在当前界面可见时才执行。那mStopped代表什么,它的值又是由谁来设置的呢?通过追踪代码,我们不难发现答案。
// Set to true if the owner of this window is in the stopped state,
// so the window should no longer be active.
boolean mStopped = false;
void setWindowStopped(boolean stopped) {
checkThread();
if (mStopped != stopped) {
mStopped = stopped;
final ThreadedRenderer renderer = mAttachInfo.mThreadedRenderer;
if (renderer != null) {
......
renderer.setStopped(mStopped);
}
if (!mStopped) {
mNewSurfaceNeeded = true;
scheduleTraversals();
}
......
}
}
从注释可知mStopped代表的是当前窗口是否处于停止状态。当窗口被setWindowStopped方法激活时,会触发scheduleTraversals操作,而它则会进一步触发performTraversals操作来执行界面渲染。
那又是谁触发了setWindowStopped呢?通过mStopped的字面意思,我们不难联想到Activity的Stop流程。在Activity的生命周期中,页面停止是从performStop方法开始的,让我们看看它到底做了什么。
final void performStop(boolean preserveWindow) {
......
if (!mStopped) {
......
// If we're preserving the window, don't setStoppedState to true, since we
// need the window started immediately again. Stopping the window will
// destroys hardware resources and causes flicker.
if (!preserveWindow && mToken != null && mParent == null) {
WindowManagerGlobal.getInstance().setStoppedState(mToken, true);
}
mFragments.dispatchStop();
mCalled = false;
mInstrumentation.callActivityOnStop(this);
if (!mCalled) {
throw new SuperNotCalledException(
"Activity " + mComponent.toShortString() +
" did not call through to super.onStop()");
}
......
mStopped = true;
}
......
}
碰巧的是Activity也有个mStopped变量用来记录当前页面是否处于停止状态,当页面 停止时,performStop首先会调用setStoppedState方法来通知WindowManagerGlobal当前页面已处于停止状态。mToken是当前窗口的身份标识,在Activity初始化时赋值。
public void setStoppedState(IBinder token, boolean stopped) {
ArrayList<ViewRootImpl> nonCurrentThreadRoots = null;
synchronized (mLock) {
int count = mViews.size();
for (int i = count - 1; i >= 0; i--) {
if (token == null || mParams.get(i).token == token) {
ViewRootImpl root = mRoots.get(i);
// Client might remove the view by "stopped" event.
if (root.mThread == Thread.currentThread()) {
root.setWindowStopped(stopped);
}
......
}
}
}
}
setStoppedState方法依据mToken找到与当前窗口关联的ViewRootImpl,接着再调用它的setWindowStopped方法来同步页面的停止状态。那页面又是何时从停止状态转变为活跃状态的呢?答案自然很明显,一定是在resume这个阶段。
final void performRestart(boolean start, String reason) {
......
if (mToken != null && mParent == null) {
// No need to check mStopped, the roots will check if they were actually stopped.
WindowManagerGlobal.getInstance().setStoppedState(mToken, false /* stopped */);
}
......
}
至此,整个逻辑就变得清晰起来。当页面的可见状态变化时,Activity会通过WindowManagerGlobal的setWindowStopped方法来向ViewRootImpl同步当前页面的停止状态,而ViewRootImpl只在页面可见时才会去执行渲染操作。
要想让页面在不可见时依然能够渲染,我们只需要在页面停止时通过WindowManagerGlobal的setWindowStopped方法将ViewRootImpl的mStopped重置为false即可。由于WindowManagerGlobal是隐藏的API,所以需要通过反射的方式来调用,调用的位置需要放在Activity的performStop方法之后,具体可以参照ActivityInterceptor。
对于包含Fragment的页面来说,我们还要关注Activity对Fragment的状态同步。
final void performPause() {
......
mFragments.dispatchPause();
......
}
final void performStop() {
......
mFragments.dispatchStop();
......
}
可以看到Activity在暂停和停止时,都会通过FragmentController来向Fragment同步对应的状态。为了防止Fragment在暂停和停止时执行某些操作影响布局树的完整性,可以去hook FragmentController的dispatchPause和dispatchStop方法实现,阻止Activity对Fragment的状态同步。阳哥是借用AOP框架来直接将这两个方法实现置空,具体可以参照FragmentController。
通过以上这两步操作即可以让Android应用页面在不可见时依然被“渲染”。这里为啥要加个引号,因为之前在介绍performTraversals的执行流程时,我曾提到performDraw仅在当前界面可见时才执行,而这个可见性并没有合适的方法来改变,所以更确切来说,本文介绍的方法可以实现Android应用页面在不可见时依然被测量和布局。
好了,到这里就分享完了。至于本文提到的方法可以用来干什么,大家就仁者见仁智者见智了。欢迎大家留言讨论,也欢迎大家关注我的公众号:阳哥说技术