App 的启动速度是用户的第一体验,互联网中有一个八秒定律,如果用户等待八秒App 还没打开,70%的用户都会停止等待
一、启动分类
官方 App startup time
- 冷启动
耗时最多,衡量标准
- 热启动
最快。 后台~前台
- 温启动
较快。只会重走activity的生命周期,不会走进程的创建以及Application的创建和生命周期
冷启动流程
- 用户点击
- 触发IPC 操作
- Process.start 进程创建
- ActivityThread 是每个单独进程的入口,会有一个main方法,进行消息循环的创建以及handler的创建
- bindApplication 通过反射创建Application 以及调用application的生命周期
- Activity 的生命周期 LifeCycle
- ViewRootImpl 开始真正的界面绘制
冷启动之前(这个过程无法干预)
- 启动App
- 加载空白Window
- 创建进程
随后任务
- 创建Application
- 启动主线程
- 创建MainActivity
- 加载布局
- 布置屏幕
- 首帧绘制
优化方向
Application 和Activity 的生命周期
二、启动时间的测量方式
adb 命令方式
adb shell am start -W packagename/首屏Activity
特点:
- 线下使用方便,不能带到线上
- 非严谨,精确时间
手动打点方式
启动时埋点,启动结束埋点,二者差值
特点:精确,可带到线上,推荐使用
避开误区
- 启动时间开始的位置在application 中的attachBaseContext
- 启动时间结束的位置采用采用Feed 第一条展示
- addOnDrawListener 要求Api 16
ThisTime 最后一个Activity启动耗时
TotalTime 所有Activity 的启动耗时
WaitTime AMS启动Activity 的总耗时
三、启动优化使用到的工具
traceview,systrace
- 两种工具互相补充
- 正确认识工具及不同场景选择合适的工具
traceview
- 优点
- 图形的形式,展示代码执行的时间,调用栈信息等
- 信息全面,包含所有线程信息
- 使用方式
Debug.startMethodTracing(“文件名”);默认8M大小,如果想要更大,传参bufferSize
Debug.stopMethodTracing() 结束
就会生成一个文件,位置在sd卡 :Android/data/packagename/files
在Android Studio 右边有一个Devices File Explorer 可以很方便的打开手机系统的文件
添加开启和结束的代码后,然后运行,在存储的位置下,刷新,会生成一个设置的文件名.trace 文件
- 如何分析
-
- 左上是通过代码精确指定的时间范围,左下角有个时间搓,不是特别重要
- 左下是线程信息,可以看到线程的总数,也可以看到每个线程在具体的时间做了哪些事
右边有四个Tab
- Top Down
total:总时间
self :
children:
举例:调用了A函数,整体时间是total,在A函数中调用了一行代码,然后执行B函数,它的selfTime是执行了一行代码的时间,childrenTime就是B函数执行的时间,selfTime和childrenTime之和一定等于totalTime
函数的调用列表,点击相应的jump to souse 可以跳入详细的代码中
ThreadTime 一定会变少,CPU执行的时间
Wall Clock Time 代码发生在这个线程上,真正执行的时间
- Call chat
每一行显示的是函数调用的时间段,垂直方法被调用着
系统Api 调用颜色是橙色
应用自身的函数调用是绿色
第三方Api 调用是蓝色
- Flame chat 火焰图
倒置的调用图表,会收集相同的调用顺序
- Bottom up
谁调用了我,和Top down 是相反的
总结:
运行时开销严重,整体都会变慢这个工具太强大了,会抓去所有线程的所有执行函数以及顺序
可能会带偏优化方向
traceview 和cpu profiler
traceview 的好处可以在代码中进行埋点,用cpu profiler 进行分析
单纯的用cpu profiler 来抓取精确的启动位置,几乎不可能
systrace
结合Android内核的数据生成html 报告
Python脚本
- 使用方式
• python systrace.py -t 10 [other-options] [categories]
- 官方文档:
https://developer.android.com/studio/command-line/systrace#command_options
- 使用案例
python /Users/Liuzhao.Future/Library/Android/sdk/platform-tools/ systrace/systrace.py -b 32768 -t 5 -a com.optimize.performance -o performance. html sched gf view wm am app
- 代码中使用
开启:TraceCompat.beginSection(“apponCreate ”)
结束:TraceCompat.endSection
- 分析html 文件
CPU核数跟不同手机是有关系的,有些手机厂商8个核都给你,有的手机8核,但是只给你使用4核
- 总结:
- 轻量级,开销小,埋了哪一点就去做哪一点
- 直观反映CPU利用率
cpuTime 和 wallTime
- wallTime 是代码执行的时间
- cpuTime 是代码消耗CPU的时间(重点指标)
举例:为什么两者会不一样
锁冲突,比如一个线程执行需要获取锁A,但是锁A被其他线程持有,没有得到释放,而这个线程是一个轻量级的,只占用了cpu 一点时间,所以这个时候cpuTime 就会很短,而wallTime 很长
四、如何优雅的获取CPU耗时
需要知道具体哪个方法占用了大量时间
- 常规方式:手动埋点
- 侵入性强
- 工作量大
- AOP方式:Aspect Oriented Programming ,面向切面编程
- 针对同一类问题的统一处理
- 无侵入添加代码
Aspect使用
- classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
- implementation 'org.aspectjaspectjrt:1.8.+'
- apply plugin: 'android-aspectix'
介绍
- Join Points
- 程序运行时的执行点 ,可以作为切面的地方
- 函数调用、执行
- 获取、设置变量
- 类初始化
- PointCut
- 带条件的JoinPoints
- Advice
- 一种Hook,要插入代码的位置
- Before : PointCut之前执行
- After :PointCut之后执行
- Around: Pointcut之前、之后分别执行
- 语法简介
//Before:Advice,具体插入位置 //execution :处理Join Point的类型 //(* android.app.Activity.on**(.)):匹配规则 @Before("execution(* android.app.Activity.on** (.))") public void onActivityCalled (JoinPoint joinPoint) thr ows Throwable { …… }
- 使用案例
@Aspect public class Performanceaop{ Around("call(*com.optimize.performance.PerformanceApp.**(..))") public void getTime(ProceedingJoinPointjoinPoint){ Signature signature=joinPoint.getSignature(); String name=signaturetoShortString(); long time=System.currentTimeMillis(); try { joinPoint.proceed(); } catch(Throwable throwable){ throwable.printStackTrace(); } LogUtils.i(msg:name+" cost "+(System.currentTimeMillis() - time)); } }
- 优点
- 无侵入性
- 修改方便
五、异步优化
- 常规异步优化:使用线程池进行异步优化
- 启动器(异步启动优化的最优解)
常规异步方式
常规异步方式需要注意点:
- 并不是所有的代码都可以直接异步
- 不符合异步要求:有的任务必须在主线程中执行
- 需要在某阶段完成,在splash界面就要用到异步任务,在执行界面的时候,异步任务还没完成某一些代码必须在某一个阶段完成,解决方案:CountDownLatch 相当于自己加了个锁
- 区分CPU 密集型还是IO密集型任务
常规异步方案痛点:
- 代码不够优雅
- 场景不好处理(依赖关系),在特定的时间内结束某个任务
- 维护成本高
启动器方式
核心思想:
充分利用CPU 多核,自动梳理任务顺序
启动器流程:
- 代码Task化,启动逻辑抽象为Task
- 根据所有任务的依赖关系生成一个有向无环图
- 多线程按照排序后的优先级依次执行
六、延迟初始化
- 常规方案:handler.postDelay
- 更优方案:对延迟任务进行分批初始化
常规方案的问题:
- 时机不便控制
- 导致Feed卡顿
更优方案的优点:利用了IdleHandler特性,空闲执行
- 执行时间明确
- 缓解Feed卡顿
七、总结:启动优化总方针
- 异步 ,延迟,懒加载
- 技术和业务相结合
八、注意事项
- 收敛启动代码修改权限
- 结合Ci修改启动代码需要Review 或 通知
九、其它方案
- 提前加载SharedPreferences
在multidex之前加载,充分利用此阶段CPU
覆写getApplicationContext返回this
- 启动阶段不启动子进程
子进程会共享CPU资源,导致主进程CPU 紧张
注意启动顺序,App onCreate 之前是ContentProvider
- 类加载优化,提前异步类加载
- 启动阶段抑制GC
- CPU锁频