一、App启动优化介绍
1、背景介绍
- 第一体验
- 八秒定律
2、启动分类
- 冷启动
- 耗时最多,衡量标准
ClickEvent
->IPC
->Process.start
->ActivityThread
(单独app进程入口类) ->bindApplication
(通过反射创建Application以及调用与Application相关的生命周期) ->LifeCycle
(Activity生命周期) ->ViewRootImpl
(开始真正的界面绘制)
- 热启动,最快
后台
->前台
- 温启动,较快
LifeCycle
(只会重走activity生命周期,不会重新进程创建)
3、相关任务
- 冷启动之前:启动App,加载空白Window,创建进程
- 随后任务:创建Application,启动主线程,创建MainActivity,加载布局,布置屏幕,首帧绘制
4、优化方向
- Application和Activity生命周期
二、启动时间测量方式
1、adc命令
adb shell am start -W packagename/首屏Activity
- ThisTime:最后一个Activity启动耗时
- TotalTime:所有Activity启动耗时
- WaitTime:AMS启动Activity的总耗时
问题:线下使用方便,不能带到线上,非严谨精确时间
2、手动打点
启动时埋点,启动结束时埋点,差值
public class LaunchTimer {
private static long sTime;
public static void startRecord() {
sTime = System.currentTimeMillis();
}
public static void endRecord() {
long cost = System.currentTimeMillis() - sTime;
Log.i("cost", cost + "");
}
}
这个回调是应用程序能接收到的最早的回调,在这里开始计时
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
LaunchTimer.startRecord();
MultiDex.install(this);
}
误区:onWindowFocusChanged只是首帧时间,并不能代表界面已经展示出来了,要在真实用户展示时间埋点,比如在列表第一条数据展示时候
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (position == 0 && !mHasRecorded) {
mHasRecorded = true;
holder.itemView.getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
LaunchTimer.endRecord();
return false;
}
});
}
}
三、 启动优化工具选择
1、traceview
- 图形化界面形式展示代码执行时间,调用栈等
- 信息全面,包含所有线程
- 使用方式
//在开始时调用
Debug.startMethodTracing("文件名");
//结束时调用
Debug.stopMethodTracing();
生成的文件存放在sd卡:Android/data/packagename/files,运行app后开路径下找到文件
双击打开文件
最上面的是时间范围,可移动鼠标拖动
中间的THREADS是线程,可以看到有17个线程,选中一个线程就可以看到下面线程做的事情,当前看的是mian线程
- Call Chart
每一行都是一个方法的调用时间消耗,他的垂直方向是被调用者
系统api是橙色,应用自身的方法是绿色,第三方api是蓝色(包括java语言的api) - TopDown
可以看到方法执行的总时间Total,Self时间和Children时间
可以选择看Wall Colock Time (线程真正执行时间),Thread Time (CPU执行时间) -
Flame Chart
收集相同的调用顺序
-
Bottom Up
一个函数的调用列表,谁调用了我
- 问题:加入了traceview,运行开销严重,整体开销都会变慢,可能会带偏优化方向
2、systrace
- 结合Android内核的数据,生成Html报告
- 使用方式
python systrace.py t 10 [other-options] [categories]
//开始时
TraceCompat.beginSection("systrace");
//结束时
TraceCompat.endSection();
到systrace目录下运行脚本,我的是E:\SDK\platform-tools\systrace
注意python要2.7版本的
python systrace.py --time=10 -o mytrace.html sched gfx view wm
也可以用Android Device Monitor来生成
打开生成的html
该报告列出了呈现UI帧并指示沿时间线的每个渲染帧的每个进程。用绿色框架圆圈表示在16.6毫秒内渲染以保持每秒60帧稳定所需的帧。渲染时间超过16.6毫秒的帧用黄色或红色框架圆圈表示。
可以选中一个线程查看他的时间消耗
- 官方文档:https://source.android.com/devices/tech/debug/systrace
- 优点:轻量级,开销小,直观反馈CPU利用率
- cputime和walltime的区别:cputime是代码消耗cpu的时间(重点指标),walltime是代码执行时间。
举例:锁冲突,可能某个线程在等待锁,导致walltime时间看起来很长,但是对CPU没有占用
3、优雅获取方法耗时
1.常规方式:
背景:需要知道启动阶段所有方法耗时
实现:手动埋点
入侵性强,工作量大
2.AOP
Aspect Oriented Programming,面向切面编程,针对同一类问题的统一处理,无侵入添加代码
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
使用AspectJ,在根build.gradle下配置:
buildscript {
...
dependencies {
...
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
}
app项目的build.gradle及新建的module的build.gradle里都应用插件
apply plugin: 'android-aspectjx'
dependencies {
implementation 'org.aspectj:aspectjrt:1.8.9'
}
- Join Points
程序运行时的执行点,可以作为切面的地方。(函数调用、执行,获取、设置变量,类的初始化) - PointCut
带条件的JoinPoints - Advice
一种Hook,要插入代码的位置。before:PointCut之前执行。after:PointCut之后执行。around:之前之后分别执行 - 语法介绍
@Before("execution(*android.app.Activity.on**(...))
public void onActivityCalled(JoinPoint joinPoint) throws Throwable{
...
}
Before:Advice,具体插入位置
execution:处理Join Point的类型,call(插入在函数体里面)、execution(插入在函数体外面)
(android.app.Activity.on*(...)):匹配规则
onActivityCalled:要插入的代码
使用AOP实现启动时间监听
@Aspect
public class PerformanceAop {
//Around在每个方法执行之前和之后分别插入代码,只切了BaseApplication里面的函数**(..)任何方法参数
@Around("call(* com.test.common.BaseApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name=signature.getName();
long time = System.currentTimeMillis();
Log.i("cost", System.currentTimeMillis() - time + "");
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i("cost", name + (System.currentTimeMillis() - time));
}
}
有点:无侵入性,修改方便
四、异步优化
1、优化小技巧
-
Theme切换:感觉上的快,先进入一个闪屏页
先使用这个theme,在MainActivity的super.onCreate()之前切换回来
2、异步优化
- 核心思想:子线程分担主线程任务,并行减少时间
异步优化注意 - 不符合异步要求的,一种是修改代码使其满足,一种是放弃异步的优化
- 需要在某个阶段完成,可以使用CountDownLatch
- 区分CPU密集型和IO密集型任务
public class AppApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//线程池数量,可以参照AsyncTask,
int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work
int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
ExecutorService service=Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(new Runnable() {
@Override
public void run() {
initBugly();
}
});
service.submit(new Runnable() {
@Override
public void run() {
initMap();
}
});
......
}
}
当然并不是所有代码都可以满足异步优化,比如在子线程中
Handler handler = new Handler();
就会崩溃,因为在子线程中他找不到looper,解决方案
Handler handler = new Handler(Looper.getMainLooper());
但是项目中,总是会有一些代码必须要在主线程中执行,这种情况就要放弃异步优化。
比如初始化的代码必须在application的onCreate()中结束,在activity中调用不到就会崩溃,针对这种情况可以使用CountDownLatch
//条件被满足1次
private CountDownLatch countDownLatch=new CountDownLatch(1);
@Override
public void onCreate() {
super.onCreate();
service.submit(new Runnable() {
@Override
public void run() {
initMap();
//条件被满足
countDownLatch.countDown();
}
});
//如果条件没满足就等待
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//条件被满足了,onCreate()才会执行结束
}
以上常规的异步方案,是有一些问题的:代码不够优雅维护成本高,有些操作有依赖关系,需要有先后顺序执行的,不方便统计
3、异步优化升级
启动器介绍
核心思想:充分利用CPU多核,自动梳理任务顺序
启动器流程
-代码Task化,启动逻辑抽象为Task
- 根据所有任务依赖关系排序生成一个有向无环图
-
多线程按照排序后的优先级依次执行
4、更优秀的延迟初始化方案
常规方案
- new Handler().postDelayed
- 痛点:时机不受控制,可能导致卡顿
更优方案 - 核心思想:对延迟任务进行分批初始化
利用IdleHandler特性空闲执行
public class DelayInitDispatcher {
private Queue mDelayTasks = new LinkedList<>();
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll();
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty();
}
};
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
5、优化总方针
- 异步,延迟,懒加载
- 技术、、业务相结合
注意事项 - wall time和cpu time
cpu time才是优化方向,按照systrace及cpu time跑满cpu - 监控的完善:线上监控多阶段时间(App,Application,生命周期间隔时间),处理聚合看趋势
- 收敛启动代码修改权限,结合ci修改启动代码需要Review或通知
其他方案 - 提前加载SharePreference
如果不加限制,可能有几十个类在使用几十个文件,调用get方法会异步加载配置文件,加载到内存当中,在get或者put一个属性时候如果load到内存中的操作没有执行完,就会阻塞进行等待
Multidex之前加载,利用此阶段的CPU
覆写getApplicationContext()返回this - 启动阶段不启动子进程
很多App会有多个进程,子进程会影响主进程的启动时间,因为子进程会共享CPU资源,导致主进程CPU紧张。
注意启动顺序:App onCreate之前是ContentProvider, - 类加载优化:提前异步类加载
每只用一个类都是通过classloader,如果启动太多类,会延迟启动时间。
通过Class.forName()只加载类本身及其静态变量的引用类,如果new类实例可以额外加载类成员变量的引用类,主要需要根据业务情况判断。
哪些类需要提前异步类加载,可以通过替换系统的ClassLoader,在自定义的ClassLoader打印log - 启动阶段抑制GC
- CPU锁频
6、模拟问题
- 你启动优化怎么做的?要讲整个过程
分析现状,确认问题:比如,在某个版本发现启动速度变得特别卡顿,用户反馈变多,所以进行优化,对启动代码进行梳理,发现启动流程非常复杂了,通过一系列工具来确认出来在主线程中执行了太多代码。
针对性优化:比如进行了异步初始化,比如有些代码优先级不是很高,可以延迟执行
长期保持优化效果: - 是怎么异步的,异步过程中遇到了哪些问题
体现演进过程:最初采用普通异步方案,之后发现不方便,寻找新的解决方案,介绍启动器 - 做了启动优化,觉得有哪些容易忽略的点
cpu time,wall time
注意延迟初始化是优化
介绍下黑科技,比如类加载,cup拉高频率 - 版本迭代导致启动变慢有什么好的解决方案
启动器,结合CI,监控完善