呕心沥血总结了一篇tips!!!最近在做需求时,遇到需要在activity渲染完成后获取页面最终展示内容,并保存成图片至本地。第一种方式是截图,第二种是直接获取decorview的内容。综合考虑后决定采用第二种方式获取当前页面内容。
问题来了,在哪个时机获取当前绘制完成view内容呢?结合自己以及网络上的方法总结了如下几种方式,。分别对每种方式的做法、结果以及中间涉及到的原理做简要的归纳总结,目的是总结出tips让大家避坑。
1、当前想到的是在Activity执行到onresume时调用view的post方法,post一个runnable到主线程,在runnable里面获取当前页面具体内容。这种方式也是最先想到的,但实际上测试结果,并没有拿到页面最终渲染后的内容,仅拿到布局背景图,而上层自定义view的内容没有拿到。这也强化了activity生命周期到onresume时,视图可见,但这里的可见,实际上并不是指view渲染完成这二者的区别。经过测试,在view的post方法里面,我们也看到仅仅拿到view的宽和高。
深入看下底层的原理: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时,获取当前页面内容。 结果是,获取到的当前内容并没有完全绘制完,不过只是这个回调方法时间点很接近了。
具体原因是:在方法一的分析流程里面,可以看到在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函数。
这种方法和方法四类似,正常业务开发中写这样的代码(骚操作)会显得逻辑比较奇怪。
文末
要想成为架构师,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
一、架构师筑基必备技能
1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……
二、Android百大框架源码解析
1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程
三、Android性能优化实战解析
- 腾讯Bugly:对字符串匹配算法的一点理解
- 爱奇艺:安卓APP崩溃捕获方案——xCrash
- 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
- 百度APP技术:Android H5首屏优化实践
- 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
- 携程:从智行 Android 项目看组件化架构实践
- 网易新闻构建优化:如何让你的构建速度“势如闪电”?
- …
四、高级kotlin强化实战
1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》
-
从一个膜拜大神的 Demo 开始
-
Kotlin 写 Gradle 脚本是一种什么体验?
-
Kotlin 编程的三重境界
-
Kotlin 高阶函数
-
Kotlin 泛型
-
Kotlin 扩展
-
Kotlin 委托
-
协程“不为人知”的调试技巧
-
图解协程:suspend
五、Android高级UI开源框架进阶解密
1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
六、NDK模块开发
1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习
七、Flutter技术进阶
1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)
…
八、微信小程序开发
1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取【保证100%免费】↓↓↓