目录:
一、App启动方式
二、App启动时间度量
三、启动优化辅助工具
四、优雅获取方法耗时
五、App启动速度优化
一、App启动方式
冷启动:App启动前,该App进程还没有创建,例如在安装后的第一次启动、设备重启或者应用被杀死情况下发生。
热启动:当App启动时,后台已经有该App的进程,例如按Home键退出App。热启动因为会从已有的进程中来启动,不会再走Application的初始化了。
温启动:介于冷启动和温启动之间,有许多潜在状态可视为温启动。例如
用户在退出应用后又重新启动应用。进程可能已继续运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。
系统将您的应用从内存中逐出,然后用户又重新启动它。进程和 Activity 需要重启,但传递到 onCreate() 的已保存的实例
state bundle 对于完成此任务有一定助益。
这个温启动的概念有些文章会有,但是并不重要,因为我们优化App启动速度是针对冷启动而言的。
这三种概念详细请看官方文档 https://developer.android.google.cn/topic/performance/vitals/launch-time#cold
二、App启动时间度量
1、adb shell方式:
通过adb命令执行am命令启动App并出界应用启动时间。
命令格式:
adb shell am start -W packagename/packagename.首页Activity
示例:
adb shell am start -W com.example.demoapp/com.example.demoapp.MainActivity2
结果:
➜ DemoApp adb shell am start -W com.example.demoapp/com.example.demoapp.MainActivity2
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.demoapp/.MainActivity2 }
Status: ok
Activity: com.example.demoapp/.MainActivity2
ThisTime: 967
TotalTime: 967
WaitTime: 1011
Complete
ThisTime:表示一连串启动 Activity 的最后一个 Activity 的启动耗时;
TotalTime:表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用Activity pause的耗时。
WaitTime:返回从 startActivity 到应用第一帧完全显示这段时间. 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;
2.adb logcat 方式
Android 4.4之后,Android在系统Log中添加了Display的log信息,可以通过过滤ActivityManager(或者是ActivityTaskManager,跟android版本有关,源码做了改动)关键字,抓取logcat 中的启动时间信息。注意这里的时间不包括数据的加载,因为很多应用在加载时会启动懒加载模式,技术局获取会后再刷新显示UI,所以如果需要获取全部时间包括数据加载时间,需要在你的activity代码的数据加载完的回调函数中加上 reportFullDrawn()。最终抓的logcat 的信息会如下图所示。
3.手动打点计时
启动时埋点,启动结束埋点,二者差值
建一个 LaunchTimer 类来记录APP启动和结束的时间差
import android.util.Log;
public class LauncherTimer {
private static long time;
public static void startRecord() {
time = System.currentTimeMillis();
}
public static void endRecord(String msg) {
long cost = System.currentTimeMillis() - time;
Log.d("LauncherTimer", msg + " cost time = " + cost);
}
}
在 Application 的 attachBaseContext(attachBaseContext 是应用启动过程中我们所能接触到第一个方法) 方法中记录启动开始时间,如下所示
import android.app.Application;
import android.content.Context;
import com.mumuxi.testapplication.android.utils.LauncherTimer;
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
LauncherTimer.startRecord();
}
@Override
public void onCreate() {
super.onCreate();
}
}
在记录APP启动结束时间是可以在 onWindowFocusChanged 方法中记录。onWindowFocusChanged是Activit的首帧时间,是activity首次绘制的时间。不过如果有时候有重要视图需要数据延迟加载后再显示的话,可以选择在数据加载后,第一次显示UI时再记录APP启动结束时间。毕竟我们是为了改善用户的体验,并不是单纯的为了把启动时间缩短,数据延迟加载导致的重要视图未显示这一点也在我们的优化范围内。
如果是列表展示的情况,我们可以选择在真实数据展示,Feed 第一条展示出来,记录启动结束时间。如下所示:
public class NewsAdapter extends RecyclerView.Adapter {
/**
* 是否已经统计过
*/
private boolean mHasRecorded;
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
//onBindViewHolder 回调多次,而只统计一次,加个变量标识
if (position == 0 && !mHasRecorded) {
mHasRecorded = true;
holder.layout.getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
holder.layout.getViewTreeObserver().removeOnPreDrawListener(this);
LauncherTimer.endRecord("FeedShow");
return true;
}
});
}
}
}
三、启动优化工具
1.Android Studio CPU Profile(推荐使用,方便快捷)
第一步:如图点击选择Edit Configurations
第二步:如图选择:(第一步后弹出如下框进行相关选择)
相关解释:Java/Kotlin Method Sample :对Java方法进行采样有可能某个方法采集不到,所以选择 Java/Kotlin Method Trace(跟踪采样)
第三步:如图选择Profile app运行
第四步 :App已经运行到我们想要的效果后,点击stop, 运行结束 如图
第五步进行分析:
详细请参考 Android Studio CPU profiler性能分析工具介绍和使用详解
四、优雅获取方法耗时
我们在做启动优化的时候通常需要知道启动阶段所有方法的耗时,这样可以有针对性的分析出耗时较多的方法。通过一些工具我们是可以查看到方法的耗时,但是通过工具我们无法做到直观的两组优化前后的各方法耗时的数据对比。启动优化工具只是帮助我们看到哪些方法耗时,整个调用链是怎么样的,这样我们就能从中找到优化点在哪里。找到优化点后,我们再自动记录优化前后的各个方法的耗时,保存起来导出excel 来直观地对比。
对于一般的获取方法的耗时的方式是通过手动埋点来实现,然后我们可以把这些数据记录到文件中,然后再做对比。比如在某个方法开始和结束的位置分别插入以下代码:
long time = System.currentTimeMillis();
initJpush();
long cost = System.currentTimeMillis() - time;
当有多个方法需要埋点时,同理这样写就可以获取到每个方法的执行时间了,但是这样操作存在的问题也是显而易见的,当然我相信你肯定也发现了,主要总结为以下几点:
- 代码重复、耦合度高并且看起来非常恶心
- 侵入性强
- 工作量大
那么针对这种方式的劣势,如何才能更加优雅的实现获取方法的耗时呢?答案就是采用AOP的方式来实现。AOP 方案的实现,有兴趣请自行了解
谈谈Android AOP技术方案
Android AOP — AspectJ的使用
五、App启动速度优化
优化总方针:
- 懒加载、延迟加载、异步加载,梳理业务选择对应技术。
- 优化体验,设计启动屏幕页改善启动体验,同时在这过程中实现对一些网络数据、数据库文件的异步预加载。
- 技术上的优化。
1.懒加载:
核心思想:需要用到时才加载。下面列具一些:
- 布局懒加载 ,不必在启动时展示的view 可以 通过ViewStub实现。
- ViewPager 懒加载
- 一些重量级的对象的初始化,也实现懒加载。
2.延迟初始化
核心思想:不阻碍启动过程,找到合适的时机再去加载。
难点:对于合适的加载时机难以把控。
如Handler 的 postDelayed这种延迟一个时间来加载(不建议使用,设置的时间难以把控,后续项目的迭代更新也可能导致这个时间需要修改,难以把握,如果不把这个时间的设置逻辑写出来的话,可读性差,难以维护)
ui展示第一帧后开始加载。在Activity的onWindowFocusChange()方法第一次回调后执行。
数据列表展示后。Feed 第一条数据后执行。缺点:用户可能在滑动列表,可能会导致卡顿。
利用MessageQueue 的 空闲任务 IdleHandler 机制 来实现延迟加载。
推荐此方案,执行时机明确,执行的时机是在系统空闲的时候进行执行,有效缓解列表卡顿,它可以真正的提升用户的体验,不过需要注意queueIdle方法回调的执行也是会阻塞线程的,如果任务耗时比较长,可以选择建立任务队列,一个一个任务地执行。任务队列过长,可能会导致任务不执行或者执行时间过长,因为queueIdle的回调是MessageQueue 的 next()第一次轮训时,message 为空才执行一次。详细请看 MessageQueue 空闲任务 IdleHandler 机制 。
/*** 延迟初始化分发器 */
public class DelayInitDispatcher {
private Queue mDelayTasks = new LinkedList<>();
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
if (mDelayTasks.size() > 0) {
mDelayTasks.poll().run();
}
return !mDelayTasks.isEmpty();
}
};
public DelayInitDispatcher addTask(Runnable task) {
mDelayTasks.add(task);
return this;
}
public void start() {
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
3.异步加载
核心思想:子线程分担主线程任务,并行减少时间。
难点:说到子线程,那大家想到的肯定是线程池了,但是如果任务间存在依赖关系,我们就无法简单地直接把任务添加到线程池就能完成。即使你把有依赖关系的任务,合并到同一个任务去,在可读性上也比较差。所以有以下方案的推荐:
阿里开源的一个启动器库alpha:https://github.com/alibaba/alpha
启动器介绍:
核心思想:充分利用CPU多核,自动梳理任务顺序
启动器流程:
- 代码Task化,启动逻辑抽象为Task
- 根据所有任务依赖关系排序生成一个有向无环图(自动生成的)
- 多线程按照排序后的优先级依次执行
4.优化体验实现预加载
方案:
主要是从交互设计上来优化,例如刚启动app时,可以设置 SplashActivity,可以设置欢迎页、展示App logo。进入SplashActivity,大约经过1~2秒再跳转到程序的主界面。并且我们在可以在SplashActivity 的 onWindowFocusChange()方法回调后做一些其他初始化工作,达到预加载的效果。目前很多App都是这么做的。当然也可以直接在拉起MainActivity后先展示logo 、欢迎页等,等待1-2秒再展示真正的界面,并在onWindowFocusChange()方法回调后做一些其他初始化工作,达到预加载的效果。
https://blog.csdn.net/yywan1314520/article/details/51622154
5.技术优化
xml 布局优化
启动阶段不要启动子进程,子进程会共享cpu资源,导致主进程cpu紧张
避免在Application的构造函数、attachBaseContext()、onCreate()内做耗时操作;一些数据预取放在异步线程执行。
ContentProvider 的 onCreate()内不要做耗时操作,Application onCreate 之前是ContentProvider(顺序为attachBaseContext -> ContentProvider -> onCreate )。对于只是为了自动初始化而利用ContentProvider 的情况,使用Jetpack 的 Startup 也可以实现启动优化。详细请看下面的文章:
Jetpack新成员,App Startup一篇就懂Activity 的onCreate()、onResume()内不要做耗时操作,ui显示第一帧后会回调onWindowFocusChange方法,可以考虑在这里做一些初始化工作。
避免在启动阶段导致进程GC,后台任务影响启动速度中还有还有另一个比较典型的 case 就是 GC,触发 GC 后可能会抢占我们的 cpu 资源甚至导致我们的线程被挂起,如果启动过程中存在大量的 GC,那么我们的启动速度将会受到比较大的影响。
Mutidex.install()优化(如果已经不考虑低版本Android5.0以下机器,可忽略)
Multidex优化Demo地址
抖音BoostMultiDex优化实践:Android低版本上APP首次启动时间减少80%
一些启动优化黑科技简单介绍,上网搜索相关字段可以搜索到一些文章,这里没有成熟的方案提供给大家:
- 启动阶段抑制GC
- CPU锁频
- 类加载优化
- IO优化
- 安装包重排布
参考:
抖音 Android 性能优化系列:启动优化实践
性能优化总结-Android启动速度优化
Android启动优化你真的了解吗?
深入探索Android启动速度优化(上)
深入探索Android启动速度优化(下)