Part 2 App启动优化
一 App启动优化介绍
1、背景
用户的第一体验,八秒定律(70%用户放弃等待)
2、启动分类
App startup time官方教程(https://developer.android.google.cn/topic/performance/vitals/launch-time)
冷启动
耗时最多,衡量标准
ClickEvent(用户点击) -> IPC(触发ipc) -> Process.start(进程的创建) -> ActivityThread(单独进程app的入口类似main) -> bindApplication(通过反射创建Application) -> LifeCycle(我们的activity) -> ViewRootImpl(界面的绘制)
热启动
最快
后台 -> 前台
温启动
较快
LifeCycle(只重走activity的生命周期)
3、相关任务
冷启动之前
启动app、加载空白window、创建进程(这三个都是系统行为,无法多做干涉)
随后任务
创建Application、创建主线程、创建MainActivity、加载布局、布置屏幕、首帧绘制
4、优化方向
Application和Activity的生命周期(我们开发者能控制的)
二 启动时间测量方式
1、adb命令方式
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() {
endRecord("");
}
public static void endRecord(String msg) {
long cost = System.currentTimeMillis() - sTime;
LogUtils.i(msg + "cost " + cost);
}
}
启动位置放置在Application的attachBaseContext中,结束位置放置在MainActivity中
特点:精确,可带到线上,推荐使用
误区:onWindowFocusChange只是首帧时间
正解:真实数据展示,Feed第一条数据展示
三 启动优化工具选择
1、traceview
图形形式展示执行时间、调用栈等,信息全面,包含所有线程
使用方式
第一种:使用Android studio的Android profiler点击cpu就可以查看
第二种:嵌入代码
Debug.startMethodTracing("文件名");//使用
Debug.stopMethodTracing();//结束
生成文件在sd卡:Android/data/packagename/files可以直接DeviceFileExplorer中找到
或者通过adb pull /mnt/sdcard/文件名.trace 指定文件目录中.将trace导出指定的文件夹中
参考文档:https://www.jianshu.com/p/7e9ca2c73c97 (Android性能优化—TraceView的使用)
总结:traceview运行时开销严重,整体都会变慢(会抓取所有线程所有函数的状态和时间),可能会带偏优化方向(影响某些函数),可在代码中埋点使用(使用方便)
2、systrace
结合Android内核的数据,生成Html报告。API18以上使用,推荐TraceCompat
使用方式
python systrace.py -t 10 [other-options][categories] python脚本
https://developer.android.google.cn/studio/command-line/systrace
python systrace.py -b 32768 -t 5 -a com.optimize.performance -o performance.html
参考文档:https://blog.csdn.net/itfootball/article/details/48915935 (Android性能专项测试之Systrace工具)
cputime与walltime的区别(cputime是优化的核心)
walltime:是代码执行的时间
cputime:是代码消耗cpu的时间(重点指标)
举例:锁冲突
总结:轻量级,开销小。直观反映cpu利用率。
四 优雅获取方法耗时讲解
1、常规方法
背景:需要知道启动阶段所有方法耗时
实现:手动埋点
开始 long time = System.currentTimeMillis();
结束 long cost = System.currentTimeMillis()-time;
总结:侵入性强,工作量大
2、AOP介绍
Aspect Oriented Programming 面向切片编程
针对同一类问题的同一处理
无侵入添加代码
AspectJ使用
classpath ‘com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0’ (项目的build.gradle下的dependencies)
implementation ‘org.aspectj:aspectjrt:1.8.+’
apply plugin: ‘android-aspectjx’
Join Points
程序运行时的执行点,可以作为切面的地方
函数调用、执行
获取或者设置变量
类初始化
PointCut
带条件的JoinPoints
Advice
一种Hook,要插入代码的位置
before:PointCut之前执行
after:PointCut之后执行
Around:PointCut之前之后分别执行
语法介绍
Before:Advice,具体插入位置
execution:处理Join Point的类型,call、execution
(* android.app.Activity.on**(…)) 匹配规则
onActivityCalled 要插入的代码
参考文档
https://www.jianshu.com/p/f577aec99e17 (关于android中使用AspectJ)
https://blog.csdn.net/qq_30682027/article/details/82493913 (AspectJ详解)
https://www.jianshu.com/p/27b997677149 (AspectJ基本用法)
3、AOP实战
实战
@Aspect
public class PerformanceAop {
@Around("call(* com.optimize.performance.PerformanceApp.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
}
}
总结:
无侵入性,修改方便
优雅获取方法的耗时,AOP的理解及使用
五 异步优化详解
1、优化技巧
1、Theme切换:感觉上的快
2、异步优化
1、核心思想:子线程分担主线程任务,并行减少时间
简单示例
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(new Runnable() {
@Override
public void run() {
//子线程中执行一个初始化的任务
}
});
这种使用会出现application走完了但是线程中初始化并没有完成而出现错误
解决办法:
private CountDownLatch mCountDownLatch = new CountDownLatch(1);//表示CountDownLatch需要被满足一次,具体多少次自己根据场景进行设置
@Override
public void onCreate() {
super.onCreate();
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
service.submit(new Runnable() {
@Override
public void run() {
//子线程中执行一个初始化的任务
}
});
service.submit(new Runnable() {
@Override
public void run() {
//子线程中执行一个初始化的任务
initWeex();
//在需要等待初始化完成的任务中调用,满足一次
mCountDownLatch.countDown();
}
});
try {
//mCountDownLatch在onCreate中最后一步进行等待
mCountDownLatch.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
异步优化注意
不符合异步要求(修改成符合异步要求,或者放弃)
需要在某阶段完成
区分cpu密集型和io密集型任务
六 异步初始化最优解-启动器
1、启动器介绍
1、核心思想:充分利用cpu多核,自动梳理任务顺序
代码Task化,启动逻辑抽象为Task
根据所有任务依赖关系排序生成一个有向无环图
多线程按照排序后的优先级依次执行
[启动器代码](链接:https://pan.baidu.com/s/1Ih1WflOlhiFO7mX4SOibtA
提取码:znse )
2、启动器实战
接口说明
public interface ITask {
/**
* 优先级的范围,可根据Task重要程度及工作量指定;之后根据实际情况决定是否有必要放更大
*
* @return
*/
@IntRange(from = Process.THREAD_PRIORITY_FOREGROUND, to = Process.THREAD_PRIORITY_LOWEST)
int priority();
void run();
/**
* Task执行所在的线程池,可指定,一般默认
*
* @return
*/
Executor runOn();
/**
* 依赖关系
*
* @return
*/
List<Class<? extends Task>> dependsOn();
/**
* 异步线程执行的Task是否需要在被调用await的时候等待,默认不需要
*
* @return
*/
boolean needWait();
/**
* 是否在主线程执行
*
* @return
*/
boolean runOnMainThread();
/**
* 只是在主进程执行
*
* @return
*/
boolean onlyInMainProcess();
/**
* Task主任务执行完成之后需要执行的任务
*
* @return
*/
Runnable getTailRunnable();
void setTaskCallBack(TaskCallBack callBack);
boolean needCall();
}
实例代码
创建task
/**
* 主线程执行的task
*/
public class InitWeexTask extends MainTask {
@Override
public boolean needWait() {
return true;
}
@Override
public void run() {
InitConfig config = new InitConfig.Builder().build();
WXSDKEngine.initialize((Application) mContext, config);
}
}
/**
* 子线程执行
* 需要在getDeviceId之后执行
*/
public class InitJPushTask extends Task {
@Override
public List<Class<? extends Task>> dependsOn() {
List<Class<? extends Task>> task = new ArrayList<>();
task.add(GetDeviceIdTask.class);
return task;
}
@Override
public void run() {
JPushInterface.init(mContext);
PerformanceApp app = (PerformanceApp) mContext;
JPushInterface.setAlias(mContext, 0, app.getDeviceId());
}
}
/**
* Application中执行的代码
*
*/
TaskDispatcher.init(PerformanceApp.this);
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
dispatcher.addTask(new InitAMapTask())
.addTask(new InitStethoTask())
.addTask(new InitWeexTask())
.addTask(new InitBuglyTask())
.addTask(new InitFrescoTask())
.addTask(new InitJPushTask())
.addTask(new InitUmengTask())
.addTask(new GetDeviceIdTask())
.start();
dispatcher.await();
七 更优秀的延迟初始化方案
1、常规方案
不需要再application中初始化的可以在Feed展示后调用,new Handler().postDelayed
@Override
public void onFeedShow() {
//如果一系列操作耗时会导致用户卡顿
new DispatchRunnable(new DelayInitTaskA()).run();
new DispatchRunnable(new DelayInitTaskB()).run();
使用postDelayed的话时间不好指定
}
总结:时机不便控制、导致界面卡顿
2、更优方案
核心思想:对延迟的任务分批初始化
利用IdleHandler的特性,空闲执行
//IdleHandler封装类
public class DelayInitDispatcher {
private Queue<Task> 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);
}
}
//使用方式
@Override
public void onFeedShow() {
DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
delayInitDispatcher.addTask(new DelayInitTaskA())
.addTask(new DelayInitTaskB())
.start();
}
总结:执行时机明确,解决界面卡顿
七 启动优化其它方案
1、优化总方针
异步、延迟、懒加载(如高德地图初始化只需要在使用的界面进行初始化即可)
技术、业务相结合
2、注意事项
walltime和cputime
cputime才是我们优化的方向
按照systrace及cputime跑满cpu
监控的完善
线上监控多阶段时间(App、Activity、生命周期间隔时间)
处理聚合看趋势
3、其他方案
提前加载SharePreferences(在Multidex之前加载,利用此阶段的cpu)
覆写getApplicationContext()返回this
@Override
public Context getApplicationContext() {
return this;
}
启动阶段不启动子进程
子进程会共享cpu资源,导致主进程cpu资源紧张
类加载优化:提前异步类加载
Class.forName()只加载类本身及其静态变量的引用类
new 类实例 可以额外加载类成员变量的引用类
哪些类需要提前异步类加载呢?(替换系统的ClassLoader,自定义的ClassLoader中添加log打印出所有的类就是需要处理的)
黑科技系列
启动阶段抑制GC(NativeHook的方案)
CPU锁频(可能会引起其他问题)