IdleHandler你会用吗?记一次IdleHandler使用误区,导致ANR
1. 示例
问题抛出,当引入线上ANR抓取工具后,发现了不少IdleHandler带来的问题。堆栈具体见下图
思考为什么idleHandler会带来这样的问题呢,或许你会觉得是单个消息执行时间过长导致的。那么请看示例,项目本身代码较为复杂,简化代码如下:
- 工具类
/**
* 添加任务到IdleHandler
*
* @param runnable runnable
*/
public static void run(Runnable runnable) {
IUiRunnable uiRunnable = new IUiRunnable(runnable);
Looper.getMainLooper().getQueue().addIdleHandler(uiRunnable);
}
- 使用工具类
public class MainActivity extends AppCompatActivity {
public static final String TAG = "idleHandler";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume: start");
//1 //关键代码处 延迟 3s执行的delay msg
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.e(TAG, "delay: msg");
startService(new Intent(MainActivity.this, MyService.class));
}
}, 3000);
//关键代码处 添加到IdleHandler里的三个任务
UIManager.run(() -> test(1));
UIManager.run(() -> test(2));
UIManager.run(() -> test(3));
}
//延迟任务
private void test(final int i) {
try {
Log.e(TAG, "queueIdle:test start " + i);
Thread.sleep(3000);
Log.e(TAG, "queueIdle:test end " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
让我们来猜猜下面代码的输出顺序。再回看一下代码,肯定以为是下面这样对吧?
如果你觉得上面的输出没问题,那就更需要继续读下去了
这里把真实log截出来:
what??delay3000ms为什么失效了。而是Idle消息先执行了。
为什么呢?不慌遇到这种问题我们肯定要通过IdleHandler机制的源码,来找答案了。
2. 源码分析
//MessageQueue.java
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
...
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
//1 执行时机
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
//2 copy副本
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
//3 逐个执行
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
//4 是否移除当前idleHandler
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}
//5 外部调用添加idleHandler
public void addIdleHandler(@NonNull IdleHandler handler) {
if (handler == null) {
throw new NullPointerException("Can't add a null IdleHandler");
}
synchronized (this) {
mIdleHandlers.add(handler);
}
}
鉴于next()方法比较长且相关介绍也比较多,这里不细说。
注释1处,这里是IdleHandler执行时机,即主线程无消息或者未到执行时机(空闲时间)。
注释2处,可以看到mPendingIdleHandlers相当于copy了mIdleHandlers中的内容,由注释5处可以看到我们调用addIdleHandler()添加的任务都存进了mIdleHandlers。
注释3处,这里循环意味着要一次性处理之前添加的全部IdleHandler任务,如果我们短时内调用了多次addIdleHandler(),意味着这些idle msg将被拿出来逐个执行;而且要全部处理掉,才可以继续执行原消息队列的消息,如果idle msg很耗时,便会出现前面的主线程的postDelay任务执行时间就非常不可靠了;同理如果期间有触摸事件发生,那极有可能会因为得不到及时处理而导致ANR发生。赶紧检查下自己的项目中是否有此类问题。
注释4处,控制本次的IdleHandler是会被再次调用还是单次调用呢,是由queueIdle()方法的返回值决定,这点是我们该利用起来的。
上面的工具方法初衷是好的,提供接口暴露给各业务侧在某个需要的时刻将非紧急任务延迟加载,进而减少卡顿,但用起来却没有那么丝滑,甚至导致ANR,希望大家理解后能够避开这个坑。
3.安全用法
IdleHandler再介绍
- 这里再赘述下IdleHandler作用,我们都知道在做启动性能优化的时候,要尽可能多地减少启动阶段主线程任务。对一些启动阶段非必须且一定要在主线程里完成的任务,我们可以在应用启动完成之后再去加载。正是考虑到这些google官方提供了IdleHandler机制来告诉我们线程空闲时机。
利用IdleHandler设计的工具类
直接使用IdleHandler,肯定不太符合博主这种“大项目”的编码风格,简单封装是必要,方便我们做些功能定制,下面给大家说下项目中的工具类。
- 正确的方法 将启动过程中非紧急的主线程任务全部放进uiTasks里,然后逐个执行,切记单个消息耗时不要太长。
private static List<Runnable> uiTasks = new ArrayList<>();
public static UIPoolManager addTask(Runnable runnable) {
tasks.add(runnable);
return this;
}
public static UIManager runUiTasks() {
NullHelper.requireNonNull(uiTasks);
IUiTask iUiTask = new IUiTask() {
@Override
public boolean queueIdle() {
if (!uiTasks.isEmpty()) {
Runnable task = uiTasks.get(0);
task.run();
uiTasks.remove(task);
}
//逐次取一个任务执行 避免占用主线程过久
return !uiTasks.isEmpty();
}
};
Looper.myQueue().addIdleHandler(iUiTask);
return this;
}
替换成上面的方法再试一遍。
@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume: start");
//1
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.e(TAG, "delay: msg");
startService(new Intent(MainActivity.this, MyService.class));
}
}, 3000);
UIManager.addTask(() -> test(1));
UIManager.addTask(() -> test(2));
UIManager.addTask(() -> test(3));
UIManager.runUiTasks();
}
得到最初希望的时序结果:
作者:Lphoenix
链接:https://juejin.cn/post/7041576680648867877
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。