我们都知道IdleHandler的含义,一般表示当前主线程在不忙碌的时候会执行IdleHandler里面的任务。
具体的内容是,当Looper在从MessageQueue中获取当前需要执行的Message时,如果当前MessageQueue中没有Message或者还没有到第一条Message执行的时间,此时MessageQueue会尝试执行IdleHandler的里面的任务,这个我们可以从MessageQueue的next方法里面得到应证:
Message next() {
//······
// 当Message为空,或者当前Message执行时间还未到
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)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
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);
}
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;
}
一般来说,我们正常使用IdleHandler都没有什么问题,queueIdle方法也会被正常的回调。但是当你做了如下操作的时候,你会发现,不管等多久,queueIdle永远不会被回调。
在View的onDraw方法里面无限制的直接或者间接调用View的invalidate方法。
做一个无限轮询的View动画。
上面我枚举了queueIdle方法不会被回调的两种场景,接下来,我们就分开来看一下。
(1). View的invalidate方法
我们在自定义View的时候,经常会手动的调用View的invalidate方法,用来保证我们的某些设置能够立即生效。但是在很多的时候,我们非常容易错误的调用了invalidate方法,从而导致陷入一种无限制重绘的状态。
举一个简单的例子,ImageView内部有setImageDrawable,setImageBitmap等设置Drawable的方法,我们在日常的开发中,也会经常用到这些方法,用来展示某些特殊的内容。
但是,一旦我们在自定义View时,在onDraw方法里面调用这些方法就会让主线程的任务队列永远不会idle。大家可以尝试一下如下的代码:
class CustomImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas?) {
setImageDrawable(context.getDrawable(R.drawable.ic_launcher_background))
super.onDraw(canvas)
}
}
在这里,我将解释为什么如上的操作会导致主线程任务队列永远不会idle。我们都知道,View的重绘流程是:invalidate -> onDraw。
也就是说,当我们在View的onDraw方法里面调用invalidate时或者其他会调用invalidate的方法(例如上面的介绍的setImageDrawable和setImageBitmap方法),最终又会执行onDraw方法,从而形成了一个类似于死递归的情形,即不断的向任务队列里面增加任务。
有人可能会想,就算不断的往任务队列里面增加任务,但是一帧的时间有那么长--16ms,不可能都在执行重绘的任务,应该总有机会idle的啊?相信很多人都会这么想,包括我在最开始的时候也是这么想的。但是仔细看了源码之后,发现自己的Android还是没有学到家。
View的invalidate方法不断的向上调用,最终会调用到ViewRootImpl的scheduleTraversals方法里面去,而在scheduleTraversals方法里面做了一件容易让人忽视的事,我们先来看一下源码:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
这里面,我们先忽略其他的操作,只看两件事:
1、调用了MessageQueue的postSyncBarrier方法,向任务队列post了一个同步屏障。这一步非常的重要,也是因为有这么一个同步屏障的存在,导致了MessageQueue不会idle。
2、往Choreographer里面post了一个traversal类型的任务,保证在下一个垂直信号到来时,可以正常的重绘View的内容。这里的mTraversalRunnable执行,最终会到CutomImageView的onDraw方法。
上面提到了同步屏障,相信大家都比较熟悉它的作用,但是我在这里还是要说明一下:
Message有一个标记位,名为FLAG_ASYNCHRONOUS,用来表示当前Message是否是异步的。而同步屏障的作用用来挡住所有同步的Message,只允许异步的Message通过。如果当前任务队列中没有异步Message,那么主线程就会休眠,直到任务队列中添加了一个异步Message,或者同步屏障被移除。
同步屏障跟普通的消息比较类似,都是一个Message对象,只是同步屏障Message的Target为空而已。
上面知道了,当调用View的invalidate时,会向任务队列里面post一个同步屏障。接下来,我们来看一下MessageQueue对同步屏障是怎么处理的,同时看一下为啥idle永远不会被调用。
Message next() {
// ······
for (;;) {
// ······
// 休眠指定时间
// ······
synchronized (this) {
// ······
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 获取异步消息
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//······
// 获取同步消息
//······
// 处理idle
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
// ·······
}
// ······
}
}
我简化了一下next方法,next方法最终的目的就是获取下一个需要被执行Message,如果获取不到的话,就会休眠。但是,当任务队列的队头(即mMessages)是一个同步屏障,这个方法里面执行流程就变得非常有意思:
1、当mMessages是同步屏障,且后续没有异步消息,那么获取异步消息和获取同步消息这两步都会失败了,即nextPollTimeoutMillis会被赋值为-1,表示无限制的休眠。
2、pendingIdleHandlerCount 默认是-1,所以会尝试着赋值。其中,由于同步屏障的存在,所以mMessages肯定不为空,同时now < mMessages.when 肯定是不成立的,因为同步屏障在ViewRootImpl的scheduleTraversals方法里面就添加进去,所以这个时间肯定比当前时间要早很多。
因此,结合上面两点,idle是不会回调的,并且会让主线程休眠,直到一个异步Message添加到队列中。这个Messgae就是Choreographer$FrameDisplayEventReceiver,我们可以简单的看一下源码:
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
// ······
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
这个Message的作用就是把Choreographer四种类型的任务全部执行,其中前面invalidate方法添加的任务就包含在这里。
到这里,我们就知道了为啥Idle永远不会回调了,我做一个总结,方便大家理解:
当我们在onDraw方法直接或者间接调用invalidate方法,ViewRootImpl会向MessageQueue里面post 一个同步屏障。
当MessageQueue轮询到这个同步屏障时,会等到Choreographer$FrameDisplayEventReceiver这个异步任务执行之后,才会执行其他任务,即才有可能触发idle。
但是Choreographer$FrameDisplayEventReceiver这个任务里面又会执行View的onDraw方法,从而形成了一个无限循环。进而,idle永远不会回调。
那么我们知道了原因所在,怎么来解决这种类似的问题呢?
原则是:尽量不在onDraw方法里面直接或者间接调用invalidate方法。如果真的要这么做,应该怎么做呢?
可以过滤无效的重绘。就拿上面的例子来说,我们可以将Drawable缓存成一个成员变量:
class CustomImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
private var mDrawable: Drawable? = null
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas?) {
if (mDrawable == null) {
mDrawable = context.getDrawable(R.drawable.ic_launcher_background)
}
setImageDrawable(mDrawable)
super.onDraw(canvas)
}
}
这里还有一个疑问,我们发现了,就算我们在onDraw方法直接或者间接调用invalidate方法,但是并不会影响我们正常的使用App,比如说使用上不能感受到明显的卡顿,这也是我们难以发现这种问题的原因。那么这是为什么呢?
其实,简单的来说,如上的操作只是每一帧的时间里面多了一个任务而已,从体验上来说,几乎没法区分这里面的差别。除非,你想要在IdleHandler处理一些事情,在这种情形下是永远不会被执行的。
(2) 无限轮询的View动画
在一个App,动画是一件再正常不过的事情,而错误的使用动画也会导致IdleHandler永远不会回调。注意,这里指的是动画是普通的View动画,而不是属性动画,属性动画没有这个问题,而针对属性动画的分析,后文会有内容介绍。
通常来说,我们会写类似如下的代码来展示一个动画:
private fun startAnimation() {
val animation = AlphaAnimation(1f, 0.5f)
animation.duration = 100
animation.repeatCount = -1
animation.repeatMode = Animation.REVERSE
val view = findViewById<View>(R.id.view)
view.startAnimation(animation)
}
上面的代码有两个特点:
是普通的View动画。
是无限轮询的动画。
如果你写了类似上面的代码,那么恭喜你,你的IdleHandler永远不会被回调。究其原因,其实还是因为无限制的调用invalidate方法,有兴趣的可以参考View 动画 Animation 运行原理解析这篇文章,了解一下View动画的实现原理,这里就不过多的介绍了。
https://www.cnblogs.com/dasusu/p/8287822.html
那么怎么解决这种问题,最简单的办法就是换成属性动画,那么肯定又有人要问了,为什么属性动画无限轮询不会影响的IdleHandler的调用呢?这就要了解属性动画的原理了,这里我简单的介绍一下,有兴趣的同学可以参考 属性动画 ValueAnimator 运行原理全解析这篇文章。
https://www.cnblogs.com/dasusu/p/8595422.html
属性动画的实现原理不同于View动画。View动画的每一帧都是通过invalidate方法来触发重绘,而属性动画每一帧的绘制都是通过Choreographer的回调实现。
本质上就是当动画开始时,会向Choreographer的任务队列里面post 一个动画类型的任务,当垂直信号到来时,会执行这里面的任务,从而回调我们的任务。
同时为了保证动画能够流畅的进行,当当前帧绘制完成,会再向Choreographer的任务队列post一个任务,保证下一帧动画能够正常绘制,从而实现了动画。
从本质上来说,属性动画少了一个很重要的步骤,就是post 一个同步屏障。在属性动画中,没有同步屏障,那么后续的任务能够继续执行,当队列中没有任务时,自然就会回调IdleHandler。