文章目录
一:应用启动类型
应用启动类型分为三种:冷启动、热启动、温启动
1.1:冷启动
简介:从点击应用图标到开始创建应用UI界面完全显示且用户可操作的全部过程。特点是耗时最多,是APP启动速度的衡量标准。
冷启动流程分析
Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl
点击应用,加载并启动APP,创建APP进程。接下来执行ActivityThread的main方法,在main方法中会执行Loop和Handler的创建,创建完成之后,就会执行到 bindApplication 方法,在这里使用了反射去创建 Application以及调用了 Application相关的生命周期,Application结束之后,便会执行Activity的生命周期,在Activity生命周期结束之后,最后,就会执行到 View的绘制。
进程的创建是系统行为,我们没办法优化,我们可以着手优化的点在Application创建,Avtivity创建,View绘制。
1.2:热启动
应用从后台切换到前台。
1.3:温启动
简介:温启动时由于app的进程仍然存在,只执行冷启动第二阶段流程
温启动常见场景:
1、用户双击返回键退出应用
2、app由于内存不足被回收
二:APP启动耗时检测
通过耗时检测,可以验证我们的优化方案是否有效和优化方案的效果。可以检测到具体的耗时任务,针对耗时任务进行优化。
1:查看Logcat
在Android Studio Logcat中过滤关键字“Displayed”,可以看到对应的冷启动耗时日志。
2:adb shell
adb shell am start -W [packageName]/[AppstartActivity全路径]
比如:adb shell am start -W com.demo/.ui.SplashActivity
3:函数插桩
原理:编辑一个统计耗时的工具类,记录某个方法的结束时间-开始时间,把方法的耗时记录到本地。然后上传到服务器。
其中需要注意的有:
在上传数据到服务器时建议根据用户ID的尾号来抽样上报。
在项目中核心基类的关键回调函数和核心方法中加入打点。
特点:精确,可带到线上,但是代码有侵入性,修改成本高。
插桩:在目标程序代码中某些位置插入或修改成一些代码,从而在目标程序运行过程中获取某些程序状态并加以分析。简单来说就是在代码中插入代码。 那么函数插桩,便是在函数中插入或修改代码。
代码:
使用
class AppApplication : CommonApplication() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
TimeMonitorManager.getInstance()
.resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START)
}
override fun onCreate() {
super.onCreate()
TimeMonitorManager.getInstance()
.getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START)
.recordingTimeTag("Application-onCreate")
//打印结果 TimeMonitorApplication-onCreate: 417
}
}
/**
* 采用单例管理各个耗时统计的数据。
*/
public class TimeMonitorManager {
private static TimeMonitorManager mTimeMonitorManager = null;
private HashMap<Integer, TimeMonitor> mTimeMonitorMap = null;
public synchronized static TimeMonitorManager getInstance() {
if (mTimeMonitorManager == null) {
mTimeMonitorManager = new TimeMonitorManager();
}
return mTimeMonitorManager;
}
public TimeMonitorManager() {
this.mTimeMonitorMap = new HashMap<Integer, TimeMonitor>();
}
/**
* 初始化打点模块
*/
public void resetTimeMonitor(int id) {
if (mTimeMonitorMap.get(id) != null) {
mTimeMonitorMap.remove(id);
}
getTimeMonitor(id).startMonitor();
}
/**
* 获取打点器
*/
public TimeMonitor getTimeMonitor(int id) {
TimeMonitor monitor = mTimeMonitorMap.get(id);
if (monitor == null) {
monitor = new TimeMonitor(id);
mTimeMonitorMap.put(id, monitor);
}
return monitor;
}
}
/**
* 耗时监视器对象,记录整个过程的耗时情况,可以用在很多需要统计的地方,
* 比如Activity的启动耗时和Fragment的启动耗时。
*/
public class TimeMonitor {
private final String TAG = TimeMonitor.class.getSimpleName();
private int mMonitorId = -1;
// 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间
private HashMap<String, Long> mTimeTag = new HashMap<>();
private long mStartTime = 0;
public TimeMonitor(int mMonitorId) {
LogUtils.d(TAG + "init TimeMonitor id: " + mMonitorId);
this.mMonitorId = mMonitorId;
}
public int getMonitorId() {
return mMonitorId;
}
public void startMonitor() {
// 每次重新启动都把前面的数据清除,避免统计错误的数据
if (mTimeTag.size() > 0) {
mTimeTag.clear();
}
mStartTime = System.currentTimeMillis();
}
/**
* 每打一次点,记录某个tag的耗时
*/
public void recordingTimeTag(String tag) {
// 若保存过相同的tag,先清除
if (mTimeTag.get(tag) != null) {
mTimeTag.remove(tag);
}
long time = System.currentTimeMillis() - mStartTime;
LogUtils.w(TAG + tag + ": " + time);
mTimeTag.put(tag, time);
}
public void end(String tag, boolean writeLog) {
recordingTimeTag(tag);
end(writeLog);
}
public void end(boolean writeLog) {
if (writeLog) {
//写入到本地文件
}
}
public HashMap<String, Long> getTimeTags() {
return mTimeTag;
}
}
4、✨AOP(Aspect Oriented Programming) 打点
面向切面编程,通过预编译和运行期动态代理实现程序功能统一维护的一种技术。
1、作用
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时大大提高了开发效率。
5、✨启动速度分析工具
5.1:TraceView
5.2: Systrace
三:应用启动优化方案
一、常见问题
(1)点击应用图标,显示白屏。
(2)首页显示太慢:初始化任务太多。
(3)首页显示后无法进行操作:太多延迟初始化任务占用主线程CPU时间片。
二、如何分析问题
1:对于启动应用白屏:我们可以设置启动背景图。
2:通过耗时分析,对代码进行针对性优化,同样也可以来验证我们优化的成果。通过过滤关键字“Displayed”,可以看到对应的冷启动耗时日志;通过函数插桩和AOP打点,统计耗时时间和上传耗时时间到服务器,分析线上启动耗时情况。通过分析工具TraceView,来查找单次执行最耗时的方法和执行次数最多的方法;
3:在APP版本升级过程中,新的版本反馈启动过慢,进行版本代码比较,分析新版本新增了哪些耗时操作。
4:对于可以异步初始化的任务:我们可以使用异步启动器在Application的onCreate方法中执行加载。
5:对于不能异步执行的,但不是必须在onCreate完成前执行的,我们可以利用延迟启动器进行加载。
6:如果任务可以到用时再加载,可以使用懒加载的方式。
7:数据缓存,缓存启动页和首页的数据等。
三、设置启动背景图
设置Activity的theme属性windowBackground,预先设置一个启动图片(layer-list实现)。避免了启动白屏和点击启动图标不响应的情况。
<style name="Splash" parent="AppCompat.FullScreen">
<item name="android:windowBackground">
@drawable/bg_splash</item>
</style>
<!--AppCompat FullScreen-->
<style name="AppCompat.FullScreen" parent="AppTheme">
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowIsTranslucent">false</item>
</style>
四、异步初始化
(1)核心思想
子线程分担主线程任务,并行减少时间。
(2)异步初始化方案演进
1、new Thread->IntentService->线程池(合理配置并选择CPU密集型和IO密集型线程池)->异步启动器
(3)异步启动器优点
1、任务Task化,启动逻辑抽象成Task(Task即对应一个个的初始化任务)。
2、根据所有任务依赖关系排序生成一个有向无环图:例如推送SDK初始化任务需要依赖于获取设备id的初始化任务,各个任务之间都可能存在依赖关系,所以将它们的依赖关系排序生成一个有向无环图能将并行效率最大化。
3、多线程按照排序后的优先级依次执行:例如必须先初始化获取设备id的初始化任务,才能去进行推送SDK的初始化任务。
原理:
初始化多个list,主要存放所有task、和先执行的task。初始化map,存放依赖关系的task。然后对list进行排序,形成一个有向无环图。然后遍历,开启runnable,来执行task任务。
源码:https://github.com/zeshaoaaa/LaunchStarter
看使用效果代码,体验代码的优雅
open class CommonApplication : BaseApplication() {
var deviceId: String? = null
override fun onCreate() {
super.onCreate()
TaskDispatcher.init(this)
val dispatcher: TaskDispatcher = TaskDispatcher.createInstance()
dispatcher
.addTask(InitCommonTask())
.addTask(InitDependsTask())
.addTask(InitMainTask())
.addTask(GetDeviceIdTask())
.start()
dispatcher.await()
DelayInitDispatcher().addTask(DelayInitTaskA()).addTask(DelayInitTaskB()).start()
LogUtils.e("CommonApplication onCreate end")
}
五、延迟初始化
原理:利用IdleHandler特性,在CPU空闲时执行,对延迟任务进行分批初始化。避免UI卡顿,提升用户体验。
在延迟启动器中,我们提供了mDelayTasks队列用于将每一个task添加进来,使用者只需调用addTask方法即可。当CPU空闲时,mIdleHandler便会回调自身的queueIdle方法,这个时候我们可以将task一个一个地拿出来并执行。这种分批执行的好处在于每一个task占用主线程的时间相对来说很短暂,并且此时CPU是空闲的,这样能更有效地避免UI卡顿,真正地提升用户的体验。
(1)延迟初始化启动器源码
原理:我们采用了IdleHandler,当前线程空闲的时候,会回调IdleHandler的queueIdle方法。
/**
* 延迟初始化启动器
*/
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);
}
}
(2)IdleHandler源码解析
(1)在ActivityThread的main方法中,会调用Looper.loop()方法。
public static void main(String[] args) {
Looper.prepareMainLooper();
Looper.loop();
}
(2)在Looper.loop()的方法中,开启循环,调用MessageQueue的next方法。
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
for (; ; ) {
Message msg = queue.next(); // might block
if (msg == null) {
//只有主动调用停止方法,才会返回null
// No message indicates that the message queue is quitting.
return;
}
}
}
(3)MessageQueue的next
//MessageQueue添加IdleHandler方法,会把我们自定义的handler添加到队列mIdleHandlers中。
public void addIdleHandler(@NonNull IdleHandler handler) {
//、、、
synchronized (this) {
mIdleHandlers.add(handler);
}
}
/**
* MessageQueue的next方法
*/
Message next() {
//1、开启死循环,不断获取msg。如果获取到msg,会return msg
for (;;) {
Message msg = mMessages;
if (msg != null) {
msg = msg.next;
return msg;
}
//2、给pendingIdleHandlerCount赋值
pendingIdleHandlerCount = mIdleHandlers.size();
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
//3、循环取出mPendingIdleHandlers中的IdleHandler
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final MessageQueue.IdleHandler idler = mPendingIdleHandlers[i];
boolean keep = false;
//4、回调IdleHandler中的queueIdle方法
keep = idler.queueIdle();
//5、如果queueIdle返回false,mIdleHandlers会删除idler。
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
}
}
总结:
1、在ActivityThread的main方法中,会调用Looper.loop()方法。
2、在Looper.loop()的方法中,开启死循环,调用MessageQueue的next方法。
3、在MessageQueue的next,也会开启一个死循环,不停的获取msg。如果msg不为空,就返回给Looper,让looper去处理msg。如果为空,会去判断mIdleHandlers是否为空。不为空,就会获取到mIdleHandlers中的我们添加的IdleHandler,会回调IdleHandler的queueIdle。如果返回false,就从mIdleHandlers这个集合删掉我们添加的IdleHandler。
源码分析:
六、页面数据预加载
在主页空闲时,将其它页面的数据加载好保存到内存或数据库,等到打开该页面时,判断已经预加载过,就直接从内存或数据库取数据并显示。
七、闪屏页与主页的绘制优化
1、布局优化。
2、过渡绘制优化。