android 如何获全屏幕view内容

呕心沥血总结了一篇tips!!!最近在做需求时,遇到需要在activity渲染完成后获取页面最终展示内容,并保存成图片至本地。第一种方式是截图,第二种是直接获取decorview的内容。综合考虑后决定采用第二种方式获取当前页面内容。

activity_view_structure.png

问题来了,在哪个时机获取当前绘制完成view内容呢?结合自己以及网络上的方法总结了如下几种方式,。分别对每种方式的做法、结果以及中间涉及到的原理做简要的归纳总结,目的是总结出tips让大家避坑。

1、当前想到的是在Activity执行到onresume时调用view的post方法,post一个runnable到主线程,在runnable里面获取当前页面具体内容。这种方式也是最先想到的,但实际上测试结果,并没有拿到页面最终渲染后的内容,仅拿到布局背景图,而上层自定义view的内容没有拿到。这也强化了activity生命周期到onresume时,视图可见,但这里的可见,实际上并不是指view渲染完成这二者的区别。经过测试,在view的post方法里面,我们也看到仅仅拿到view的宽和高。

activity_lifecycle.png
深入看下底层的原理:ActivityThread中执行handleResumeActivity方法并在里面执行了activity的onResume方法,这片段的源码如下:

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
          String reason) {
              ...
          final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
          ...
          final Activity a = r.activity;
          ...
          final Activity a = r.activity;
          ...
          //获取Window也就是PhoneWindow
           r.window = r.activity.getWindow();
           //获取PhoneWindow中的DecorView
           View decor = r.window.getDecorView();
           decor.setVisibility(View.INVISIBLE);
          ViewManager wm = a.getWindowManager();
          //获取PhoneWindow的参数
          WindowManager.LayoutParams l = r.window.getAttributes();
          a.mDecor = decor;
          l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
          l.softInputMode |= forwardBit;
          ...
            a.mWindowAdded = true;
            wm.addView(decor, l);
          ...
           Looper.myQueue().addIdleHandler(new Idler());
          }

在执行activity的onResume方法后,创建了ViewManager,然后拿到LayoutParams,最后通过addView方法把DecorView和LayoutParams加入ViewManager.ViewManager其实就是一个WindowManagerImpl对象.跟进代码里面可以看到,WindowManagerImpl 调用的addView方法又调用了mGlobal.addView()方法,mGlobal是个WindowManagerGlobal对象在成员变量中直接通过单例创建WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance()。最终的addView的代码:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
                ...

            WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

            ...

             ViewRootImpl root;
             View panelParentView = null;

        ...
          //创建一个ViewRootImpl并设置参数
          root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);
            //保存传过来的view,ViewRootImpl,LayoutParams
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            ...
            root.setView(view, wparams, panelParentView);
            ...
            }

最后其实是创建了ViewRootImpl,给传过来的DecorView置LayoutParams参数,然后放到对应的集合中缓存,最后调用root.setView方法将他们关联起来

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            ...
           requestLayout();  
            ...
           view.assignParent(this);
        }

看了这部分逻辑后,其实明确了在执行到activity的onResume方法时,只是将view的内容以及相关参数提交至DecorView,View内容的渲染并未正在开始

2、onWindowFocusChanged函数时,focus为true时,获取当前页面内容。 结果是,获取到的当前内容并没有完全绘制完,不过只是这个回调方法时间点很接近了。

view绘制的流程
具体原因是:在方法一的分析流程里面,可以看到在setView()方法里通过 requestLayout - scheduleTraversals 向 Choreographer 请求安排绘制任务。Choreographer收到VSYNC信号回调到ViewRootImpl的performTraversals对DecorView进行measure、layout、draw等view的绘制。requestLayout 之后具体的逻辑就是向WMS通过binder发起添加window的过程,WMS完成操作后会把windowFocusChanged的事件回调给应用进程,ViewRootImpl在把该事件分发给DecorView,而DecorView重载了View的 onWindowFocusChanged 方法,内部最终将消息通过接口回传给了Activity的onWindowFocusChanged。也就是当activity中收到了windowFocusChanged的方法回调时,表明view已经提交了绘制的步骤。回调onWindowFocusChanged 和执行Traversals之间是有先后顺序的,进程间通信通过子线程发消息到主线程,scheduleTraversals会向主线程消息队列插入一个屏障消息,并且在 performTraversals时才会移除该消息,期间所有抛向队列的同步消息都被阻塞,包括 windowFocusChanged 事件,所以focusChanged相对于讲在后面才被执行。

@UnsupportedAppUsage
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
        	// 注释2
            mTraversalScheduled = true;
            // 注释3
            // 插入同步屏障syncBarrier到消息队列,挡住普通的同步消息,优先执行异步消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 注释4
            
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            ...
        }
    }

3、对view添加onDrawListener,判断Decorview执行了OnDraw则认为第一帧绘制完成。onDraw执行完成时View并没有真正渲染完成,并且发现onDraw方法会回调多次。

日志打印时间
通过日志可以看到,在o'n onDrawListener里面的onDraw方法是会回调多次的,且早于自定义View的onDraw方法,所以在onDrawlistener回调的onDraw方法去获取页面内容,也不合理。 refresh_process.png

4、View执行完成onDraw后post个Message,当执行Message时认为第一帧绘制结束。

在最后一个渲染的view里面的onDraw方法里面去post一个message,这种办法验证是可以的。但其实并不是很准确,严格意义讲很难获取到页面绘制完成的准确时间,因为在view的oDraw方法执行完成后,所需要的资源提交到surfaceflinger等系统服务进行合成,这中间的时间耗时其实也是有的,在不同的机型上有所差异。

5、DecorView添加一像素的View,在onDraw函数里监听下一个vsync事件认为渲染完成。这个方法参考了网上的解法,理由是该View是DecorView的最后一个子View, 因为安卓是深度优先递归measure、layout、draw,所以该View是最后一个执行onDraw函数。

这种方法和方法四类似,正常业务开发中写这样的代码(骚操作)会显得逻辑比较奇怪。

参考文章:

wanandroid.com/wenda/show/…
zhuanlan.zhihu.com/p/194351632
codeantenna.com/a/MEGo21sNJ…
blog.csdn.net/my_csdnboke…

猜你喜欢

转载自juejin.im/post/7090158402407825416