前面两章已经介绍了统计数据指标、启动框架等准备工作,这章将开始分析如何利用工具,分析和解决问题。本章将介绍分析耗时问题和提高线程的调度。
解决耗时方法
方法耗时工具
“工欲善其事必先利其器”,所以我们需要先找到适合做启动优化分析的工具。
方法耗时检测官方提供了Method Trace工具,但是性能损耗大,得出的结果并不真实;Nanoscope 非常真实,但是需要刷定制后的 ROM 才能使用 Nanoscope;Systrace 可以很方便 地追踪关键系统调用的耗时情况,但是不支持应用程序代码的耗时分析。
Method Trace
首先我们分析Method Trace工具使用方式,Method Trace其实有两种使用方式,一种是通过Android Studio的Profile模式抓取,一种是通过代码Debug.startMethodTracing()抓取。
method.trace文件可视化图
1) Android Studio的Profile模式
这种方式只需要在开启profile模式,点击cpu,进入cpu页面后点击record。
对于启动阶段的method trace需要通过配置才能开启。
这种方式优缺点
- 优点: 不用写代码耦合。
- 缺点:trace文件抓取需要通过Profile模式,Profile模式必须在debug模式下才能进行,而且性能损耗非常大,会让问题分析有较大误差。
2)通过代码抓取
在app启动的时候startMethodTracingSampling,首页加载完成后stopMethodTracing,然后会生成trace文件,将手机中的trace文件导出到电脑上,通过profile窗口打开。
Debug.startMethodTracingSampling(traceName, 0, sampleInterval);
Debug.stopMethodTracing();
这种方式优缺点
-
优点: trace文件debug和release模式都可以抓取,release模式抓取的文件分析耗时更加准确。
-
缺点:需要在工程中添加代码,但是代码量较少。
3)分析
其实通过分析,我们发现通过代码模式下的relase模式method trace其实对性能影响还好,低端机下开启method trace后整体启动耗时仅增长300ms。
Systrace
还有一种方案是“systrace + 函数插桩”的方案,打包过程通过对所有方法插装上Trace节点,生成Trace信息文件。使用gradle自动化增加Trace Tag
class Trace {
public static void i(String tag) {
Trace.beginSection(name);
}
public static void o() {
Trace.endSection();
}
}
方案选择
其实分析了上面的两种方式,我们将对不同的场景采用不同方式。
- 本地开发快速分析问题通过
Debug.startMethodTracingSampling方式,调试效率高。 - 针对非常细节深度的分析采用
systrace + 函数插桩的方式。
耗时排查和解决
网络数据解析耗时
通过method trace排查启动过程中相关的耗时任务,这里展示一个比较典型的场景,网络数据请求返回后主线程解析数据。这里我们可以直接放到异步中执行。
布局解析耗时
通过启动预加载,异步创建首页布局。
对于耗时大部分场景我们处理的方案就是将耗时任务放到异步线程中执行,这个时候我们需要考虑,将任务放到异步线程执行,还是会抢占CPU,那我们如何分析适不适合放到异步,或者有没有更好的处理方式,下面我们继续分析。
解决线程调度,提高执行效率
我们的核心目的是让重要的任务可以分配cpu执行;并保证任务执行过程中没有锁等待,导致线程空执行。
Systrace 是 Android4.1 中新增的性能数据采样和分析工具。它可帮助开发者收集 Android 关键子系统(如 SurfaceFlinger/SystemServer/Kernel/Input/Display 等 Framework 部分关键模块、服务,View系统等)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。
Systrace 的功能包括跟踪系统的 I/O 操作、内核工作队列、CPU 负载以及 Android 各个子系统的运行状况等。
关于 Systrace 的官方介绍和使用可以看这里:Systrace, 大神的讲解文章可以看androidperformance.com/2019/07/23/…
这里我们先整理一下简单的基础知识,每个线程上方都有一个线条,表示当前的执行状态。
- 绿色 : 运行中
- 蓝色 : 可运行
- 白色 : 休眠中
Systrace使用
Systrace通过代码的形式调用开始和结束点,必须对应。
Trace.beginSection("tag");
Trace.endSection();
解决任务依赖
1 解决依赖关系一部分是业务上的理解,有些代码知道有明显的依赖关系,比如下载模块依赖http模块,这块不做详细的介绍。
2 排查一些比较难分析的依赖, 解决锁问题。
我们将启动的所有任务开始和结束打上Trace埋点,查找线程线上线条为白色的地方,表示当前现在被休眠,放大后点击下面monitor的字样,下面窗口会显示当前线程锁等待在了see-network-1的getInterface的方法,这个时候我们结合Method Trace的调用栈,去对应线程see-network-1搜索一下这个方法,就可以看到清晰的调用栈,这个时候我们将getInterface方法提前,将当前任务配置到getInterface对应任务的后面
解决CPU抢占问题
上面也讲到了,我们将大量耗时任务异步后将会抢占CPU,这个时候我们关注线程执行状态的蓝色部分,表示当前任务可执行,但是CPU未执行。
为了好理解我在这里做个假设,HttpTask、AccountTask这两个任务必须在app create 执行完成,但是现在看两个任务的状态,上面状态条有部分蓝色线条,这个时候我们把trace图滚动到最上方,看当前时间点cpu分配在哪些任务上面,为什么没有执行当前任务,其他被执行的线程任务是否必要。
分析完成后,我们的重心就是保证重要任务被CPU执行,下面介绍2种方式
1 提升cpu分配的执行时间片
通过提高重要任务线程优先级,提升cpu分配的执行时间片
android.os.Process.setThreadPriority(-10);
2 将任务打散执行
将任务拆分到多个阶段执行
3 启动阶段不启动子进程
子进程会共享CPU资源,导致主进程CPU紧张
总结
启动优化总结就是三板斧,异步、懒加载和预加载。需要通过工具合理的把这三板斧耍起来,达到千变万化,万变不离其宗。
本章主要总结通过工具解决卡顿和线程调度问题,分析的是业务主流的问题解决方案,后面会将分享一些黑科技的优化、治理和防护方案。
很感谢您阅读这篇文章,如果能给你带来帮助, 帮忙关注一下 Github,一起分享知识。