「性能优化1.3」延迟加载方案
「性能优化1.0」启动分类及启动时间的测量
「性能优化1.1」计算方法的执行时间
「性能优化1.2」异步优化
「性能优化1.3」延迟加载方案
一、延时加载
1.1、为什么要延迟加载?
我们在 MainActivity 中优先应该展示视图给用户,而一些其它的数据可以将其延迟再去初始化,例j如我们一般会在进入 MainActivity 时去检测一下当前是否是新用户来确定是否要显示引导图,或者读取当前未读的消息等,这些操作要求的及时性并不是那么高,这样就不会影响视图的展示。
1.2、延迟加载的方案是什么?
- 常规实现
在 MainActivity#OnCreate 执行一个 postDelayed(Runnable r, long delayMillis)
- 更优方案
使用 IdleHandler
1.3、常规方案
postDelayed(Runnable r, long delayMillis)这种方案的伪代码如下:
//MainActivity.java
public void onCreate(...) {
...
mainHandler.postDelay(new Runnable(){
public void run() {
//具体要做的延迟加载
//例如读取未读消息
showTipPopWindow();
//用户当前登录状态等
checkUnReadMsg();
}
},500);
...
}
从伪代码中可以看出,这种方式确实可以做到数据的延迟加载,但是其缺点是很明显的:
时间不好把控,不能确定 delay 多少时间。如果时间设置过短,那么此时 UI 还没渲染完毕,这势必会阻塞到 UI 的渲染,如果过长,那么又导致延迟时间变长了。
1.4、更优方案
1.4.1、IdleHandler 处理延迟加载
源码位置:MessageQueue.IdleHandler
先列出来 IdleHandler 的源码。
/**
* Callback interface for discovering when a thread is going to block
* waiting for more messages.
*/
public static interface IdleHandler {
/**
* Called when the message queue has run out of messages and will now
* wait for more. Return true to keep your idle handler active, false
* to have it removed. This may be called if there are still messages
* pending in the queue, but they are all scheduled to be dispatched
* after the current time.
*/
boolean queueIdle();
}
我们先来描述一下关于 Handler ,Looper,MessageQueue,Message 这几个东西的作用。
每一个 Looper 都会绑定一个线程,,而且 Looper 会不断的轮训对应的 MessageQueue 去获取需要处理的 Message,当前消息队列没有更多消息可以处理的话(也就是当前是空闲状态),那么系统就会告诉 IdleHandler,我们只需要在 queueIdle() 方法中处理我们做的任务。
当我们处理好我们的一个任务之后,queueIdle() 返回 true 表示我们要继续使用这个 IdleHanlder,下次 MessageQueue 空闲时,还是会继续回调 queueIdle() 方法,我们继续在这里处理我们未完成的任务即可,如果返回 false ,那么系统就会移除这个 IdleHandler 。
1.4.2、核心思想是什么?
分批进行延迟加载,每一次 Handler
空闲时就加载一个任务。
配合这个示例图和下面的代码来理解如何使用 IdleHanlder 实现延迟初始化。
延迟初始化.png
- 延迟加载的两个任务
//显示引导框UI
public void showTipPopWindow(){
//一顿操作异步操作,然后更新 UI 视图
}
//加载消息红点UI
public void checkUnReadMsg(){
//一顿操作异步操作,然后更新 UI 视图
}
- 将任务的执行放到 Runnable 中,保存到一个集合列表中 tasks
final List<Runnable> tasks = new ArrayList<>();
tasks.add(new Runnable() {
@Override
public void run() {
showTipPopWindow();
}
});
tasks.add(new Runnable() {
@Override
public void run() {
checkUnReadMsg();
}
});
- 定义一个 IdleHandler
MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
Log.d(TAG, "queueIdle");
if (!tasks.isEmpty()) {
//取出一个任务
Runnable task = tasks.get(0);
//执行这个任务
task.run();
//执行完毕,移除这个人
tasks.remove(task);
}
//如果任务列表为空,就不要这个 IdleHandler 了,
return !tasks.isEmpty();
}
};
//往主线程的 MessageQueue 中添加我们自定义的 IdleHandler
Looper.myQueue().addIdleHandler(idleHandler);
- 在 MainActitity 中第一个 View 绘制时调用
我们上一节中已经了解了如何获取应用的启动时间,我们是通过打点的方式来计算这个时间差值,开始打点时间为 Application#attachBaseContext,而结束打点的位置就是第一个 View的 onPreDrawListener 回调时。具体不清楚的读者可以回到上一小节看一下具体过程。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.textview);
textView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
Log.d(TAG, "queueIdle");
if (!tasks.isEmpty()) {
//取出一个任务
Runnable task = tasks.get(0);
//执行这个任务
task.run();
//执行完毕,移除这个人
tasks.remove(task);
}
//如果任务列表为空,就不要这个 IdleHandler 了,
return !tasks.isEmpty();
}
};
//往主线程的 MessageQueue 中添加我们自定义的 IdleHandler
Looper.myQueue().addIdleHandler(idleHandler);
return true;
}
});
}
好了,经过上面几步的操作,我们就已经将需要延迟加载的任务放到 IdleHanlder 中去处理,这种方案解决了上面通过 Handler.postDelay()不能确定具体需要 delay 的时间问题,并且因为任务的及时性要求不是很高,那么就可以等主线程比较空闲时再来执行,一举两得。
二、总结
本节中,我们了解为什么要进行任务的异步加载,以及实现了两种异步加载方案,并对比了两种方案的优劣性,最后我们得到一个更加优的方案就是利用系统提供的 IdleHandler 来处理我们的延迟任务。在本节中最重要的是需要理解好延迟加载的核心思想就是在主线程``空闲时每次只加载一个任务。
记录于 2019年3月19日