一、APP启动流程
首先在《Android系统和APP启动流程》中我们介绍了 APP 的启动流程,但都是 FW 层的流程,这里我们主要分析一下在 APP 中的启动流程。要了解 APP 层的启动流程,首先要了解 APP 启动的分类。
1、启动分类
冷启动
应用从头开始启动,即应用的首次启动。需要做大量的工做,耗费的时间最多。
热启动
当活动有驻留在内存中,系统只是把该活动放到前台,无需重复对象初始化、布局扩充和呈现。例如按了home键,相对于冷启动,开销较低。
温启动
用户退出应用程序,随后又从新启动,可是活动的进程是有驻留在后台的。例如按了back键退出应用。
2、生命周期
冷启动
对于冷启动的耗时计算比较复杂,除了 Activity 本身的生命周期外,还要考虑 Application 中
onCreate 的耗时操作。所以对于冷启动来说,一般从进程创建(即
Application 的attachBaseContext 方法)开始计时,到
完成视图的第一次绘制(即 Activity 的 onWindowFocusChanged 方法)停止计时。
冷启动 Activity 生命周期:
onCreate() -> onStart() -> onResume() -> onWindowFocusChanged()
热启动
相比于冷启动,热启动省去了 Application 的初始化,以及 Activity 的 onCreate。热启动生命周期:
Home:onPause() -> onStop()
热启动:onStart() -> onResume()
温启动
温启动生命周期:
Back:onPause() -> onStop() -> onDestory()
热启动:onCreate() -> onStart() -> onResume()
可以看到温启动相比热启动,Activity 生命周期增加了一个 onCreate() 方法,在使用 Back 时也比 Home 多执行一个 onDestory()。
二、启动时间统计
1、埋点统计
根据上面的生命周期,在不同方法中打印当前时间,通过查看 Log 计算不通启动方式的启动耗时。
2、查找Log
通过查找 Log 关键字 Displayed 查看 APP 入口信息,即包含启动时间。
adb命令
adb logcat | grep "Displayed"
结果输出
I/ActivityManager( 384): Displayed com.test.myapp/com.test.myapp.MainActivity: 1734 ms (total 1734 ms)
3、adb命令统计
adb shell am start -W
包名/启动类的全限定名
实际使用
adb shell am start -W com.test.myapp/com.test.myapp.MainActivity
冷启动需要杀掉最近任务进程,再使用上面的命令。热启动需要 Home 键退出应用后使用上面命令。
结果输出
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.test.myapp/.MainActivity }
Warning: Activity not started, its current task has been brought to the front
Status: ok
LaunchState: HOT
Activity: com.test.myapp/.MainActivity
ThisTime: 79
TotalTime: 79
WaitTime: 82
Complete
LaunchState:启动方式 COLD(冷启动)、 HOT(热启动)和 WARM(温启动)。
ThisTime:启动多个 Activity 的最后一个 Activity 的启动耗时。
TotalTime:启动多个 Activity 总耗时,包括新进程的启动和 Activity 的启动。也就是说,开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。
WaitTime:应用进程的创建过程 + TotalTime,就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间。
如果只关心某个应用自身启动耗时,参考TotalTime;如果关心系统启动应用耗时,参考WaitTime;如果关心应用有界面Activity启动耗时,参考ThisTime。
多次测试
adb shell am start -S -R 10 -W com.test.myapp/com.test.myapp.MainActivity
其中 -S 表示每次启动前先强行停止,-R表示重复测试次数,注意反斜杠、包名、类名。所以可以通过 -S 设置冷/热启动。
三、方法耗时统计
traceview 统计可以用代码统计。也可以用 Android Studio自带的 cup profiler 来统计。
1、Debug Trace
java
@Override
public void onCreate() {
super.onCreate();
Debug.startMethodTracing("main_trace");
......
Debug.stopMethodTracing();
}
生成文件路径:storage/emulated/0/Android/data/packagename/files/main_trace。
然后直接将该文件拖入到 Android Studio 中,就能看到对应方法执行的时长,从而进行相关的优化,如是否可以在异步线程操作该方法或者考虑懒加载的方式,根据自己的业务找到合适的方法。
2、CPU Profiler
Profiler 是 Android Studio 内置的工具。如下配置:
1)run -> edit configurations;
2)勾选start recording a method trace on startup;
3)从菜单中选择cpu记录配置(profiling菜单下勾选两个复选框);
4)apply --> profile模式部署。
详细使用参考:Android app的启动优化总结
3、systrace
java
@Override
public void onCreate() {
super.onCreate();
Trace.beginSection("Launcher");
......
Trace.endSection();
}
命令行终端进入如下目录:
/Users/tian/Library/Android/sdk/platform-tools/systrace
输入如下命令进入监听状态:
python systrace.py -o main_trace sched freq idle am wm gfx view binder_driver hal dalvik camera input res
此时运行代码,完成之后在命令行窗口按 Enter
键结束监听,然后会生成目标文件 main_trace,同样使用 Android Studio 的 Profiler 工具进行分析。
三、优化策略
1、优化onCreate, onStart,onResume函数
由于许多内容在 Activity 的 UI 初始化和生命周期中需要用到,所以大部分 Activity 中的成员需要在 onCreate 中通过 new 的方式赋值。这就要求 new 的类的构造函数应该尽可能简单,不要有耗时操作,以便快速执行。
不要在这些函数中 new 暂时用不到的内容,比如一些提醒的 dialog,可以在需要提醒的地方再去创建。
2、优化布局文件
减少UI的布局嵌套层数,从而减少 layout 时间。 简化XML布局,界面布局时,层次越多,加载的时间就越长。因此应该尽可能的减少布局层次。如果实在层次太多并且无法简化,建议不使用XML布局,直接在代码中进行布局。
判断嵌套布局是否可以优化的方法:
1)借助工具Hierarchy Viewer,可以看到layout比较耗时的节点。
2)直接review xml布局文件。
布局优化方案:
1)尽量使用 ConstraintLayout 和 RelativeLayout 替换 LinearLayout。
2)尽量为所有分辨率创建资源,减少不必要的硬件缩放,这会降低UI的绘制速度。
3)首次不需要显示的节点,尽量设置为GONE。
3、优化draw过程
去掉不必要的背景,比如如果子节点和父节点size一样,那么父节点的background可以不设或者设为null。
尽可能少用或者不用高质量图片,以提高运行效率。
4、优化数据访问
有些属性需要在 onCreate 就获取,而这些属性保存在 ContentProvider 中。可以从下面两方面进行优化:
1)少用 cursor.getColumnIndex。可以在建表的时候用 static 变量记住某列的 index,直接调用相应index而不是每次查询。
2)查询时返回更少的结果集及更少的字段。只返回需要的字段和结果集,更多的结果集和字段会消耗更多的时间及内存。
5、优化自定义控件或UI部件
自定义控件和UI部件,不管这些控件是否支持 xml 化,实现它们的代码质量很重要,要尽可能简化它们的构造过程。
6、代码方面的优化
1)使用缓存。尽量将需要频繁访问或访问一次消耗较大的数据存储在缓存中。
2)使用多线程。比较耗时的过程,尽可能的使用异步加载。避免UI主线程阻塞,发生长时间不响应。
3)只需要获取图片的高宽时,可以设置 InJustDecodeBounds 为 true。这样就不会去 decode 图片,减少了图片解析的时间。
4)判断语句如果较多时,尽量使用 switch..case..,而不是使用 if..else..。因为 if..else.. 是从上到下进行判断,而 switch..case.. 有对判断条件进行优化。
5)for() 循环中有 if() 判断,考虑实现为将 if() 判断语句放在 for() 语句外面,减少判断次数,for 语句可以快速执行。
6)String的拼接尽量使用io流。
7)数据类型和数据结构的选择。比如:hash 系列数据结构查询速度更优,ArrayList 存储有序元素。
7、延迟执行
对于 onCreate, onStart,onResume 函数中的数据或变量初始化,不着急使用的可以放到使用时在进行初始化,或者放到子线程中处理。
8、空闲时间利用
合理利用程序的空闲时间,等待页面都完全渲染完毕之后再执行。例如:使用IdleHandler
,具体使用如下:
java
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 执行你的任务
return false;
}
});
空闲时执行多个任务:
java
public class DelayTaskQueue {
private final Queue<Runnable> mDelayTasks = new LinkedList<>();
private final MessageQueue.IdleHandler mIdleHandler = () -> {
if (mDelayTasks.size() > 0) {
Runnable task = mDelayTasks.poll();
if (task != null) {
task.run();
}
}
// mDelayTasks非空时返回ture表示下次继续执行,为空时返回false系统会移除该IdleHandler不再执行
return !mDelayTasks.isEmpty();
};
public DelayTaskQueue addTask(Runnable task) {
mDelayTasks.add(task);
return this;
}
public void start() {
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
9、锁优化
锁是我们解决并发的重要手段,但是如果滥用锁的话,很可能造成执行效率下降,更严重的可能造成死锁等无法挽回的场景。
当我们需要处理高并发的场景时,同步调用尤其需要考量锁的性能损耗:
1)能用无锁数据结构,就不要用锁。
2)缩小锁的范围。能锁区块,就不要锁住方法体;能用对象锁,就不要用类锁。
10、内存优化
内存优化的核心是避免内存抖动。不合理的内存分配、内存泄漏、对象的频繁创建和销毁,都会导致内存发生抖动,最终导致系统的频繁 GC。
1)解决应用的内存泄漏问题。这里我们可以使用LeakCanary 或者 Android Profile 等工具来检查我们查询可能存在的内存泄漏。平时编码应当注意避免内存泄漏。
2)如避免全局静态变量和常量、单例持有资源对象(Activity,Fragment,View等),资源使用完立即释放或者recycle(回收)等。
3)避免创建大内存对象,频繁创建和释放对象(尤其是在循环体内),频繁创建的对象需要考虑复用或者使用缓存。
4)加载图片可以适当降低图片质量,小图标尽量使用SVG,大图/复杂的图片考虑使用webp。尽量使用图片加载框架,如glide,这些框架都会帮我们进行加载优化。
5)避免大量bitmap的绘制。
6)避免在自定义View的onMeasure、onLayout和onDraw中创建对象。
7)使用 SpareArray、ArrayMap 替代 HashMap。
8)避免进行大量的字符串操作,特别是序列化和反序列化。不要使用+(加号)进行字符串拼接。
9)使用线程池(可设置适当的最大线程池数)执行线程任务,避免大量Thread的创建及泄漏。
11、线程优化
当我们创建一个线程时,需要向系统申请资源,分配内存空间,这是一笔不小的开销,所以我们平时开发的过程中都不会直接操作线程,而是选择使用线程池来执行任务。所以线程优化的本质是对线程池的优化。
12、IO优化
IO优化的核心是减少IO次数。
网络请求优化
1)避免不必要的网络请求。对于那些非必要执行的网络请求,可以延时请求或者使用缓存。
2)对于需要进行多次串行网络请求的接口进行优化整合,控制好请求接口的粒度。比如后台有获取用户信息的接口、获取用户推荐信息的接口、获取用户账户信息的接口。这三个接口都是必要的接口,且存在先后关系。如果依次进行三次请求,那么时间基本上都花在网络传输上,尤其是在网络不稳定的情况下耗时尤为明显。但如果将这三个接口整合为获取用户的启动(初始化)信息,这样数据在网络中传输的时间就会大大节省,同时也能提高接口的稳定性。
磁盘IO优化
1)避免不必要的磁盘IO操作。这里的磁盘IO包括:文件读写、数据库(sqlite)读写和SharePreference等。
2)对于数据加载,选择合适的数据结构。可以选择支持随机读写、延时解析的数据存储结构以替代 SharePreference。
3)避免程序执行出现大量的序列化和反序列化(会造成大量的对象创建)。