美文网首页
android 如何获全屏幕view内容

android 如何获全屏幕view内容

作者: 蜗牛是不是牛 | 来源:发表于2022-04-27 15:15 被阅读0次

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

    activity_view_structure

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

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

    深入看下底层的原理: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方法去获取页面内容,也不合理。

    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…

    相关文章

      网友评论

          本文标题:android 如何获全屏幕view内容

          本文链接:https://www.haomeiwen.com/subject/szqlyrtx.html