2.App启动优化
1
2
3
4
5
//App启动优化
1.获取启动耗时:
1.手动埋点,aop统一处理,systrace/traceview获取
2.优化手段:延迟加载或者异步加载(充分利用CPU,将任务放到子线程中执行分担主线程任务,jetpack的App Startup框架,或者自定义使
用线程池异步加载)

一、启动优化介绍

背景介绍

1
2
1.第一体验(应用启动的速度是第一感受)
2.八秒定律(如果一个页面8秒还没有响应,70%的用户将放弃等待)

启动分类

1
2
3
4
App startup time
- 冷启动
- 热启动
- 温启动

冷启动: 耗时最多,衡量标准 (经历了一系列的流程,因此耗时较多)

image-20230914162857993

热启动:最快 image-20230914163028372

温启动:较快 (重走activity的生命周期,不会重走进程的创建或application的创建等)image-20230914163202924

相关任务

1
2
3
4
5
6
7
8
9
10
11
12
1.冷启动之前(系统的操作)
- 启动App
- 加载空白Window
- 创建进程
2.随后任务
- 创建Application
- 启动主线程
- 创建MainActivity
3.随后任务
- 加载布局
- 布置屏幕
- 首帧绘制

优化方向

1
Application和Activity生命周期这个阶段

二、启动时间的测量方式

1
2
1.adb命令
2.手动打点

adb命令

1
2
3
4
5
6
7
adb shell am start -W packagename/首屏Activity
- ThisTime:最后一个Activity启动耗时
- TotalTime:所有Activity启动耗时
- WaitTime:AMS启动Activity的总耗时
缺点:
- 线下使用方便,不能带到线上
- 非严谨、精确时间
image-20230914163945871

手动打点

1
2
3
1.精确,可带到线上,推荐使用
2.避开误区,采用Feed第一条展示
3.addOnDrawListener要求API16
1
2
3
4
5
6
7
8
9
10
11
//方法类
public class LaunchTimer {
private static long sTime;
public static void startRecord() {
sTime = System.currentTimeMillis();
}
public static void endRecord(){
long cost = System.currentTimeMillis() - sTime;
Log.e( "tyl","cost=" + cost);
}
}
1
2
3
4
5
6
7
8
startTime的执行地方:
- Application的attachBaseContext方法中
endTime的执行地方:
- 误区: activity的onWindowFocusChanged只是首帧时间,用户还没有看到画面
- 正解:真实数据展示,Feed第一条展示 (recycleView的adapter的onBindViewHolder()->
holder.linearLayout.getViewTreeObserver().addOnPreDrawListener()->
onPreDraw()中执行
//onWindowFocusChanged和Feed中执行结束相差100多ms

三、启动优化工具选择

1
2
1.traceview
2.systrace

注意

1
2
- 两种方式互相补充
- 正确认识工具及不同场景选择合适的工具

traceview和systrace各自的作用和区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Traceview 和 Systrace 都是用于 Android 应用程序性能分析和调试的工具,但它们在功能和用途上有一些区别:

Traceview:
Traceview 是 Android 开发工具包(SDK)中提供的一种效率分析工具,用于分析应用程序的方法调用、线程执行时间等信息。
开发人员可以使用 Traceview 工具来查看应用程序的性能数据,包括方法的执行时间、CPU 使用率、线程的执行情况等。
Traceview 会生成一个包含方法调用图、执行时间和线程等信息的 trace 文件,开发人员可以通过 Android Studio 或其他工具进行分析和
查看。
主要用于分析应用程序代码的性能瓶颈、方法执行耗时等问题,帮助优化应用程序的性能。

Systrace:
Systrace 是 Android 系统级的跟踪工具,用于分析整个系统的性能数据,包括应用程序、系统服务、内核等的运行情况。
开发人员可以使用 Systrace 工具来查看应用程序与系统之间的交互情况、系统资源占用情况、视图绘制耗时等信息。
Systrace 通过采集系统事件、函数调用、线程、CPU 使用情况等信息,生成一个详细的时间线,展示系统运行时的各种活动。
主要用于分析应用程序与系统交互导致的性能问题,如绘制卡顿、I/O 操作慢、系统资源竞争等,帮助优化应用程序与系统的性能。

总的来说,Traceview 主要用于分析应用程序代码的性能问题,而 Systrace 则更侧重于分析整个系统的性能问题,包括应用程序与系统之
间的交互。开发人员可以根据具体情况选择合适的工具进行性能分析和调试,以优化应用程序的性能和稳定性。

traceview

1
2
3
traceview的优点:
图形的形式展示执行时间、调用栈等
信息全面,包含所有线程

使用方式(查看指定方法执行时间)

1
2
3
4
5
6
7
8
9
10
11
1.String tracPath=getExternalCacheDir()+"/myTrac.trace";
Debug.startMethodTracing(tracPath);//tracPath 生成文件的文件路径,需要读写权限
........
要测试的方法
......
2.Debug.stopMethodTracing();
3.生成文件在sd卡://tracPath=storage/emulated/0/Android/data/com.withub.android.cloudsharingcourt/cache/myTrac.trace
//如在application的oncreate()方法中第一行加入Debug.startMethodTracing("");,
//在oncreate()最后一行加入Debug.stopMethodTracing();
4.点击在androidStudio的右侧有个Device File explorer,这个工具是方便打开手机中的文件;
//根据传入的指定路径找到生成trace文件

trace文件打开后如下图:

traceView
traceView
traceView

底部4个菜单栏的含义:

1.CallChart:垂直方向是方法的调用栈(如A调用B则B在A的下面),另外系统API的颜色是橙色,应用自身的代码调用是绿色的,其他第三方的api调用则是蓝色;

image-20230918101722306

2.FlameChart:火焰图,收集相同调用顺序完全相同的函数,相对callChart作用要小很多;

image-20230918101758594

3.TopDowm:函数的调用列表,相当于CallChart的文字版;

选中方法点击右键可以点击Jump to Source跳转到对应代码;

可以查看指定方法的执行时间,Total=self+Children,self等于执行这个A方法的时间children则是A方法内其他方法的所用时间;

右上角有个下拉选项:Wall Clock Time(所在线程真正所用的时间),threadTime(cpu所执行的时间)

image-20230918101925336

4.BottomUp:和FlameChar类似重要性不高,显示的方法的被调用列表,点击B查看B被谁调用的;

image-20230918102230075

traceview的缺点

1
2
3
1.运行时开销严重,整体都会变慢
2.可能会带偏优化方向
3.traceview与cpu profiler(2者结合来分析)

systrace

1
2
3
4
5
6
7
8
9
10
11
12
1.结合Android内核的数据,生成Html报告
2.API18以上使用,推荐TraceCompat
3.使用方式
python systrace.py -t 10 [other-options] [categories]
//官方文档:https://developer.android.com/studio/commandline/systrace#command options

代码中埋点:
TraceCompat.beginSection("AppOncreate");
........
要测试的方法
......
TraceCompat.endSection();

脚本示例:

image-20230918104122465

-b 收集包的大小 ,-t 时间,-a 包名,-o 输出生成的文件名

systrace默认图:image-20230918104443838

image-20230918104519592

kernel:不同时间段只运行了4核,有时8核(如何高效的运用cpu);

image-20230918104829323

可以查看不同线程的运行情况,点击具体方法可以展开方法的详细运行时间

1
2
3
4
5
6
7
8
systrace优点:
轻量级,开销小
直观反映cpu利用率
注意:
cputime与walltime区别
- walltime是代码执行时间
- cputime是代码消耗cpu的时间(重点指标)
举例:锁冲突

四、优雅获取方法耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
常规方式
-背景:需要知道启动阶段所有方法耗时
-实现:手动埋点
-long time = System.currentTimeMillis();
-long cost = System.currentTimeMillis() - time;
- 缺点:侵入性强,工作量大

AOP介绍
-Aspect Oriented Programming,面向切面编程
-针对同一类问题的统一处理
-无侵入添加代码
AOP实战
-AspectJ使用(辅助实现AOP)
-classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0//旧版本在build.gradle buildScripe->dependencies中
-implementation 'org.aspectj:aspectjrt:1.8.+
-apply plugin: 'android-aspectjx//旧版本app build.gradle的顶部

-Join Points使用
-程序运行时的执行点,可以作为切面的地方
-函数调用、执行
-获取、设置变量
-类初始化

-PointCut
-带条件的JoinPoints

-Advice
-一种Hook,要插入代码的位置
-Before : PointCut之前执行
-After:PointCut之后执行
-Around:PointCut之前、之后分别执行

-语法简介
@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 :要插入的代码

实战代码:任意自建的一个类image-20230918112134978

AOP的优点:无侵入性,修改方便

五、异步优化

优化小技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Theme切换:感官上变快了;(实际启动时间未变快)
1.
<layer-list xmIns:android="http://schemas.android.com/apk/res/androidandroid:opacity="opaque"><!-- The background color, preferably the same as your normal theme --><item android:drawable="@android:color/white"/><!-- Your product logo - 144dp color version of your app icon --><item> 4
<bitmap
android:src="@drawable/product_ogo_144dp'
android:gravity="center"/>
</item>
</layer-list>

2.
<activity ...android:theme="@style/AppTheme.Launcher”/>

3.
@Override
protected void onCreate(Bundle savedInstanceState){
// super.onCreate之前切换回来
setTheme(R.style.Theme_MyApp);
super.onCreate(savedInstanceState);
}

异步优化

1
核心思想:子线程分担主线程任务,并行减少时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MyApp extends MultiDexApplication {
//同步工具类,用来协调多个线程之间的同步,用来作为线程间的通信而不是互斥作用
private CountDownLatch countDownLatch = new CountDownLatch(1);
@Override
public void onCreate() {
super.onCreate();
//动态获取设备核心数量,老版本生效新版本未验证;
int CORE_POOL_SIZE = Math.max(2,Math.min(Runtime.getRuntime().availableProcessors() - 1,4));
//创建线程池,java原生代码传入核心数;
ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
//一个任务一个线程,多个任务则submit多个
executorService.submit(new Runnable() {
@Override
public void run() {
initTask1();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initTask2();
//initTask2执行完毕后通知countDownLatch;
countDownLatch.countDown();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initTask3();
}
});
...
try {
countDownLatch.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

异步优化注意

1
2
3
1.不符合异步要求
2.需要在某阶段完成
3.区分CPU密集型和IO密集型任务

六、异步优化启动器

常规异步痛点

1
2
3
1.代码不优雅
2.场景不好处理(依赖关系)
3.维护成本高

启动器介绍:

1
核心思想: 充分利用CPU多核,自动梳理任务顺序

启动器流程

1
2
3
1.代码Task化,启动逻辑抽象为Task
2.根据所有任务依赖关系排序生成一个有向无环图
3.多线程按照排序后的优先级依次执行

启动器流程图:image-20230918144000861

启动器起源码(启动器代码太多):https://gitee.com/cq_tyl/performance-optimization/tree/master(utils/TaskDispatcher.java)

1
2
3
4
5
6
7
8
9
10
示例:
TaskDispatcher.init( context:PerformanceApp.this);
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
dispatcher.addTask(new InitAMapTask()).addTask(new InitStethoTask())InitweexTask())
.addTask(newaddTask(newInitBuglyTask())addTask(newInitFrescoTask())
.addTask(newInitJPushTask())
.addTask(newInitUmengTask())
.addTask(newGetDeviceIdTask())
.start();
dispatcher.await();//如有需要等待完成后才继续往下走

七、更优的延迟初始化的解决方案

常规方案:将延迟操作放到显示之后执行

1
2
3
4
5
1.New Handler().postDelayed
2.Feed展示后调用
缺点:
- 时机不便控制
- 导致Feed卡顿

更优方案:对延迟任务进行分批初始化

1
利用IdleHandler特性,空闲执行

实战源码:DelayInitDispatcher.java及其引用 (https://gitee.com/cq_tyl/performance-optimization/tree/master)

八、启动优化其他方案

优化总方针:

1
2
1.异步、延迟、懒加载
2.技术、业务相结合

注意事项

1
2
3
4
5
6
7
8
1.wall time与cpu time
2.cpu time才是优化方向
3.按照systrace及cpu time跑满cpu
4.监控的完善
- 线上监控多阶段时间(App、Activity、生命周期间隔时间 )
- 处理聚合看趋势
5.收敛启动代码修改权限
- 结合Ci修改启动代码需要Review或通知

其他方案

1
2
3
4
5
6
7
8
9
10
11
1.提前加载SharedPreferences
- Multidex之前加载,利用此阶段CPU
- 覆写getApplicationContext0)返回this
2.启动阶段不启动子进程
- 子进程会共享CPU资源,导致主进程CPU紧张
- 注意启动顺序:App onCreate 之前是ContentProvider
3.类加载优化:提前异步类加载
- Class.forName0只加载类本身及其静态变量的引用类
- new 类实例 可以额外加载类成员变量的引用类
4.启动阶段抑制GC
5.CPU锁频

九、问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.你做启动优化是怎么做的
- 分析现状、确认问题
- 针对性优化
- 长期保持优化效果
2.是怎么异步的,异步遇到问题没有
- 体现演进过程
- 详细介绍启动器
3.你做了启动优化,觉得有哪些容易忽略的注意点
- cpu time与wall time
- 注意延迟初始化的优化
4.版本迭代导致的启动变慢有好的解决方式吗
- 启动器
- 结合CI
- 监控完善