如何让Android应用页面在不可见时依然被渲染

看到这个标题,你可能会想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的执行过程可以看到,performMeasureperformLayout以及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的dispatchPausedispatchStop方法实现,阻止Activity对Fragment的状态同步。阳哥是借用AOP框架来直接将这两个方法实现置空,具体可以参照FragmentController

通过以上这两步操作即可以让Android应用页面在不可见时依然被“渲染”。这里为啥要加个引号,因为之前在介绍performTraversals的执行流程时,我曾提到performDraw仅在当前界面可见时才执行,而这个可见性并没有合适的方法来改变,所以更确切来说,本文介绍的方法可以实现Android应用页面在不可见时依然被测量和布局。

好了,到这里就分享完了。至于本文提到的方法可以用来干什么,大家就仁者见仁智者见智了。欢迎大家留言讨论,也欢迎大家关注我的公众号:阳哥说技术

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值