
作者简介
罗铁锤,六年安卓踩坑经验,致力于底层平台、上层应用等多领域开发。文能静坐弹吉他,武能通宵写代码
1 Handler 发送Message 流程
从主线程 new Handler() 开始:
<Handler.java>public Handler() {this(null, false);}public Handler(@Nullable Callback callback, boolean async) {if (FIND_POTENTIAL_LEAKS) {final Class<? extends Handler> klass = getClass();if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&(klass.getModifiers() & Modifier.STATIC) == 0) {Log.w(TAG, "The following Handler class should be static or leaks might occur: " +klass.getCanonicalName());}}// 获取当前线程的 loopermLooper = Looper.myLooper();if (mLooper == null) {throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread()+ " that has not called Looper.prepare()");}// 获取当前线程的 messageQueuemQueue = mLooper.mQueue;mCallback = callback;// 回调mAsynchronous = async;// 设置异步标记,异步消息会优先处理}<Lopper.java>// 通过 ThreadLocal 保证每个线程只有一个 looperpublic static @Nullable Looper myLooper() {return sThreadLocal.get();}
主线程的 Looper 是在应用程序启动时 ActivityThread 为我们创默认创建了,所以上面 new Handler() 中可以直接获取到主线程的 Looper :
<ActivityThread.java>public static void main(String[] args) {...// 构造主线程 Looper 对象Looper.prepareMainLooper();......// 应用运行期间所有代码都运行在此位置,如果主线程 Looper 退出循环,则应用运行结束// 开启消息循环Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");}
Looper 初始化和准备过程
<Looper.java>public static void prepareMainLooper() {// 创建实例prepare(false);synchronized (Looper.class) {if (sMainLooper != null) {throw new IllegalStateException("The main Looper has already been prepared.");}// 变量赋值sMainLooper = myLooper();}}private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {// 一个线程只能有一个 Looperthrow new RuntimeException("Only one Looper may be created per thread");}// new 了一个 Looper 对象sThreadLocal.set(new Looper(quitAllowed));}// 开始死循环获取消息public static void loop() {final Looper me = myLooper();// 获取当前线程 looperif (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}final MessageQueue queue = me.mQueue;// 获取到当前消息队列// Make sure the identity of this thread is that of the local process,// and keep track of what that identity token actually is.Binder.clearCallingIdentity();final long ident = Binder.clearCallingIdentity();// Allow overriding a threshold with a system prop. e.g.// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'final int thresholdOverride =SystemProperties.getInt("log.looper."+ Process.myUid() + "."+ Thread.currentThread().getName()+ ".slow", 0);boolean slowDeliveryDetected = false;// 开启死循环轮询消息for (;;) {// 从消息队列中获取下一个未处理的消息,重点,后面分析Message msg = queue.next(); // might block// 消息队列为空,没有消息了,此时说明退出应用了if (msg == null) {// No message indicates that the message queue is quitting.return;}...try {// 消息分发回调给 handler 处理msg.target.dispatchMessage(msg);if (observer != null) {observer.messageDispatched(token, msg);}dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;}...// 回收消息加入复用池msg.recycleUnchecked();}}
new Handler() 以及主线程 Looper 的初始化和准备工作已经完成,接下去就是常见的发送消息
<Handler.java>// Message 的复用机制下面再分析handler.sendMessage(Message.obtain());// 发送消息public final boolean sendMessage(@NonNull Message msg) {return sendMessageDelayed(msg, 0);}// 发送消息如果不设置延时,内部设置为 0public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {if (delayMillis < 0) {delayMillis = 0;}// 时间的基数为当前系统启动时间,即使 delay = 0,实际上 uptimeMillis 也是 > 0return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);}// 不论是 post,sendEmptyMessage 等最终都是走到这里public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {MessageQueue queue = mQueue;// 当前线程消息队列if (queue == null) {RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");Log.w("Looper", e.getMessage(), e);return false;}// 将消息按时间放入队列中return enqueueMessage(queue, msg, uptimeMillis);}// 将消息添加到消息队列private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {// target 赋值给当前 handler,所以 message 持有当前 handler 的引用,一般情况 new Handler 匿名内部类持有外部Activity 引用,而 MessageQueue 持有 Message 引用,从而产生生命周期比 Activity 长的引用链,因此会导致 Activity 内存泄露(普通匿名内部类一般不会导致内存泄露)msg.target = this;msg.workSourceUid = ThreadLocalWorkSource.getUid();// 如果是异步消息,设置标记位FLAG_ASYNCHRONOUSif (mAsynchronous) {msg.setAsynchronous(true);}// 最终调用MessageQueue的enqueueMessage方法,将当前消息放到队列的尾部return queue.enqueueMessage(msg, uptimeMillis);}
接下去就是将消息放入消息队列,等待消息的轮询处理
<MessageQueue.java>boolean enqueueMessage(Message msg, long when) {if (msg.target == null) {throw new IllegalArgumentException("Message must have a target.");}if (msg.isInUse()) {throw new IllegalStateException(msg + " This message is already in use.");}// 注意,同步锁!为了处理多个线程中 Handler 发送消息的并发处理synchronized (this) {// 当前 Looper 正在执行 quit 操作,也是调用 MessageQueue 的 quit,此时发送消息没必要处理了if (mQuitting) {IllegalStateException e = new IllegalStateException(msg.target + " sending message to a Handler on a dead thread");Log.w(TAG, e.getMessage(), e);// 回收消息msg.recycle();return false;}msg.markInUse();// 标记当前消息正在处理msg.when = when;// 当前消息的需处理时间// ----->>>> 重点:消息入队及排序Message p = mMessages;// 当前消息队头boolean needWake;// 是否需要唤醒,一般不需要用户去主动去唤醒,除非无消息了长期休眠// 1. p = null 说明当前队列没有消息,直接插入到队头(通常这种场景)// 2. when==0 只有 Handler 调用 sendMessageAtFrontOfQueue 时成立// 3. when < p.when 说明当前消息时间小于队头消息的时间,需要插到最前面if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.msg.next = p;// msg 插入到队头:msg->p->...mMessages = msg;// 刷新当前队头needWake = mBlocked;// 上次取消息时,队列没有消息了,mBlocked=true 并且挂起,所以此刻需要唤醒} else {// 当前队列中已经存在其他未处理消息,mBlocked=false// Inserted within the middle of the queue. Usually we don't have to wake// up the event queue unless there is a barrier at the head of the queue// and the message is the earliest asynchronous message in the queue.// 同步屏障消息needWake = mBlocked && p.target == null && msg.isAsynchronous();// 一般是 falseMessage prev;// 定义上一个消息for (;;) {// 循环遍历prev = p;// 将当前队头赋值给上个消息p = p.next;// 将之前的队头的下一个消息赋值给 p// 1. msg1 -> msg2 (假设当前队列中有两个消息,msg1 是当前队头)// 2. 定义一个变量 p = msg1// 3. 定义一个变量 pre = p, 也就是 pre = msg1// 4. p = msg1, p.next = msg1.next = msg2, 所以 p = msg2// 5. 此时消息顺序:pre -> p// 6. 不断循环上述操作,pre 和 p 一直往后移动:pre 是倒数第二个,p 是队尾if (p == null || when < p.when) {// p==null说明已经是队列的尾部了// when < p.when 说明当前消息处理时间比当前队尾的消息要早,需要排到 p 的前面break;}// 一般needWake=false进不来if (needWake && p.isAsynchronous()) {needWake = false;}}// 上面循环排序后获取到的 pre 是倒数第二个消息,p 是最后一个消息// 将当前发送的消息插入到当前遍历到的队尾( p 不一定是整个队列最后的消息):// pre->msg->p || pre->msg->p->...msg.next = p; // invariant: p == prev.nextprev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {// native 层唤醒,停止 next 方法中的阻塞 nativePollOnce,立刻获取消息nativeWake(mPtr);}}return true;}
正常情况下至此,消息已经发送并且成功加入到 MessageQueue 的队尾了,接下去就是消息的轮询处理了,上面已经提到了 Looper.loop() 方法,内部是个死循环,不断调用 MessageQueue.next() 去获取下一个消息
<Looper.java>// 开启死循环轮询消息for (;;) {// 从消息队列中获取下一个未处理的消息,重点,后面分析Message msg = queue.next(); // might block// 消息队列为空,没有消息了,此时说明退出应用了if (msg == null) {// No message indicates that the message queue is quitting.return;}...try {// 消息分发回调给 handler 处理msg.target.dispatchMessage(msg);if (observer != null) {observer.messageDispatched(token, msg);}dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;}...// 回收消息加入复用池msg.recycleUnchecked();}<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;// native 层的 MessageQueue 的指针if (ptr == 0) {return null;// native 退出了或者挂了,epoll 机制已经无效了,还玩啥?}// 这个值会影响下面的 mBlocked 标记int pendingIdleHandlerCount = -1; // -1 only during first iterationint nextPollTimeoutMillis = 0;// 同样是个死循环for (;;) {if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}// 此处可能会阻塞,但是不会导致应用卡死(应用卡死是 ANR)// 阻塞过程会休眠 cpu ( linux 机制 ),节约系统资源// 阻塞不会一直延续,会在超时后自动唤醒,touch 事件或者调用 nativeWait 会主动唤醒休眠nativePollOnce(ptr, nextPollTimeoutMillis);// 注意,消息入队同步锁,取消息同样有同步锁,都是为了处理多线程并发问题// 此处也说明当前队列的消息并不是完全准确的按照 delayTime 处理,可能存在延时synchronized (this) {// Try to retrieve the next message. Return if found.final long now = SystemClock.uptimeMillis();// 当前系统启动时间Message prevMsg = null;// 定义上一个消息 prevMsgMessage msg = mMessages;// 当前消息队头// msg.target == null 说明是 postSyncBarrier 方式发送的同步屏障消息,非一般通过 handler 的 sendMessage 发送出来if (msg != null && msg.target == null) {// Stalled by a barrier. Find the next asynchronous message in the queue.do {prevMsg = msg;msg = msg.next;// 不断往后遍历,msg 为当前遍历的队尾,直到找到最前面的异步消息} while (msg != null && !msg.isAsynchronous());}if (msg != null) {// msg 为当前队头或者异步消息if (now < msg.when) {// 队头消息的时间都还没到,暂时不需要处理,计算下一次唤醒时间// Next message is not ready. Set a timeout to wake up when it is ready.nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);// 计算本次阻塞到下次唤醒的时长} else {// 到时间了,需要取出消息处理// Got a message.mBlocked = false;// 说明有消息存在,不需要阻塞if (prevMsg != null) {// 同步屏障消息的场景,msg 为最靠前的异步消息prevMsg.next = msg.next;// 取出了队列中间最前的异步消息,重新链接队列链表} else {mMessages = msg.next;// 之前的队头已经取出来处理了,队头后移一个}msg.next = null;// msg 已经从队列中取出if (DEBUG) Log.v(TAG, "Returning message: " + msg);msg.markInUse();// 标记 msg 正在使用return msg;// 取出需要处理的消息,返回}} else {// msg 为空,没有消息了,需要一直挂起// No more messages.nextPollTimeoutMillis = -1;}// Process the quit message now that all pending messages have been handled.if (mQuitting) {// 正在退出,返回 nulldispose();return null;}// 假设上面没有 return message,说明当前队列没有需要处理的消息(没有消息或者需要处理的时间未到),则开始执行 idleHandler// 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.if (pendingIdleHandlerCount < 0 // 上面初始值为 -1&& (mMessages == null || now < mMessages.when)) {pendingIdleHandlerCount = mIdleHandlers.size();// 获取 idleHandlers 容量}if (pendingIdleHandlerCount <= 0) {// 没有 idleHandler 需要执行// No idle handlers to run. Loop and wait some more.mBlocked = true;// 走到这说明没有 return message,也没有 idleHandler 需要执行,所以要阻塞// 此时 nextPollTimeoutMillis = -1 会一直阻塞直到被主动唤醒continue;}// 初始化 idleHandlersif (mPendingIdleHandlers == null) {mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];}mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);}// 执行空闲 idleHandler// 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 handlerboolean 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;// 上面已经执行过了 idleHandler,所以赋值 0,所以 for 死循环中不会再执行// 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 执行后立即唤醒,可能有延时消息到了处理时机}}
取出消息后,再回去看 Looper.loop() 里对消息的分发
<Looper.java>public static void loop() {...for (;;) {Message msg = queue.next(); // might blockif (msg == null) {// 没消息说明当前已经调用了 quit 退出循环// No message indicates that the message queue is quitting.return;}...try {msg.target.dispatchMessage(msg);// msg 的 target 就是 Handler,回调 handler 的 dispatchMessage 方法if (observer != null) {observer.messageDispatched(token, msg);}dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} catch (Exception exception) {if (observer != null) {observer.dispatchingThrewException(token, msg, exception);}throw exception;} finally {ThreadLocalWorkSource.restore(origWorkSource);if (traceTag != 0) {Trace.traceEnd(traceTag);}}...msg.recycleUnchecked();}}<Handler.java>public void dispatchMessage(@NonNull Message msg) {if (msg.callback != null) {handleCallback(msg);// 如果 msg 有自己的 callback,执行 msg 的 callback,一般情况下 callback 都为 null} else {if (mCallback != null) {// 这个 mCallback 对于 app 来说是无法使用的,google 已经添加了注解 @UnsupportedAppUsageif (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);// 所以,通常情况就走到这里了,回调消息处理}}// Message.obtain 方法有一个重载,内部有两个参数:// public static Message obtain(Handler h, Runnable callback) {...}private static void handleCallback(Message message) {message.callback.run();// 执行 callback 的 run 方法}
至此,整个消息分发处理流程已经分析结束。总结如下:
Looper.prepare -> new Looper 对象并添加到 ThreadLocal 保存
Looper 构造函数中 -> new MessageQueue 创建消息队列
Looper.loop -> 开启死循环轮询,不断调用 MessageQueue.next
MessageQueue.next() 获取下一条需要处理的消息,无消息或者还未到处理时间则阻塞休眠
Handler.sendMessage -> Handler.enqueueMessage -> MessageQueue.enqueueMessage 将消息添加到队列
Looper.loop 循环中通过 MessageQueue.next 取出发送的消息 -> Msg
Msg.target(Handler).dispatchMessage -> Handler.handleMessage
当消息队列没有消息时会持续挂起,下一条消息来了会触发主动唤醒
正常情况下系统会根据当前时间和队头消息的处理时间计算出下次唤醒的时间,不需要主动触发唤醒
2 Message 的复用机制
Message 是可以通过 new 的方式来进行创建,但是该方式就会在堆区申请内存空间, android 系统内部充斥着大量消息,如果每个消息都通过该方式进行创建,该消息处理完后会产生大量的垃圾碎片,造成内存抖动,频繁 gc ,严重影响性能。因此,内部通过一个缓存池进行复用(链表结构)
<Message.java>public static final Object sPoolSync = new Object();// 同步对象锁private static Message sPool;// 消息队列中的队头(也可以称为缓存池,message 自身是链表结构,next 指向下一条消息)private static int sPoolSize = 0;// 当前缓存池大小private static final int MAX_POOL_SIZE = 50;// 最多缓存 50 个消息@UnsupportedAppUsage/*package*/ Message next;// 享元设计模式,对象共享,只是擦除内部属性/*** Return a new Message instance from the global pool. Allows us to* avoid allocating new objects in many cases.*/public static Message obtain() {synchronized (sPoolSync) {// 同步处理if (sPool != null) {// 缓存池为空// 从 message 链表结构中取出队头的 message 给外部使用,同时将 sPool 指向新的队头Message m = sPool;sPool = m.next;m.next = null;// 擦除内部标记,断开消息链m.flags = 0; // clear in-use flagsPoolSize--;return m;// 返回该消息对象}}return new Message();}// sPool 缓存池的创建初始化,系统调用/*** Recycles a Message that may be in-use.* Used internally by the MessageQueue and Looper when disposing of queued Messages.*/@UnsupportedAppUsagevoid recycleUnchecked() {// Mark the message as in use while it remains in the recycled object pool.// Clear out all other details.flags = FLAG_IN_USE;what = 0;arg1 = 0;arg2 = 0;obj = null;replyTo = null;sendingUid = UID_NONE;workSourceUid = UID_NONE;when = 0;target = null;callback = null;data = null;synchronized (sPoolSync) {// 同步处理if (sPoolSize < MAX_POOL_SIZE) {// 当前容量小于最大容量 50next = sPool;// sPool 赋值给 nextsPool = this;// 当前消息赋值给 sPool:sPool-> 当前 msg->next... 下次复用的永远是队头的消息sPoolSize++;// 容量 + 1}}}
3 Looper 线程单例原理
核心类就是 ThreadLocal ,它提供线程局部变量,每个线程都有自己独立的一份变量,通常是类中的 private static 字段,它们希望将状态与某一个线程相关联,在多线程编程中常用,比如 Android 的绘制同步机制 Choreographer 中也有使用。
<Looper.java>@UnsupportedAppUsagestatic final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();// 会判断,如果之前 ThreadLocal已经存在 Looper 对象,抛出异常,一个线程只能有一个 Looperprivate static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));}<ThreadLocal.java>public void set(T value) {Thread t = Thread.currentThread();// 获取当前线程ThreadLocalMap map = getMap(t);// 获取当前线程的 ThreadLocalMap,默认为 nullif (map != null)// 当前线程已经初始化了 ThreadLocalMapmap.set(this, value);// key 为当前 ThreadLoaclelsecreateMap(t, value);// 延迟,第一次 set 时候才进行初始化}void createMap(Thread t, T firstValue) {// ThreadLocalMap 和当前线程绑定,保证线程唯一性t.threadLocals = new ThreadLocalMap(this, firstValue);// 创建 ThreadLocalMap,并且添加初始值}static class ThreadLocalMap {// 构造函数会存储第一次赋值ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;// 存储的值Entry(ThreadLocal<?> k, Object v) {super(k);// 弱引用缓存当前 ThreadLocal 对象value = v;// 存储的值}//Entry.get() == null 表示 key 不再被引用,表示 ThreadLocal 对象被回收}private Entry[] table;// 缓存 key-value 的数组// setter 赋值private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;// 缓存数组int len = tab.length;// 当前容量int i = key.threadLocalHashCode & (len-1);// 生成索引,新增一个就会变化for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {// 当前 ThreadLocal 对象为 key,如果相等覆盖之前的 value 值e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);// 加入缓存int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}// getter 取值private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);// 生成索引,规则同 set 方法Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}}public T get() {Thread t = Thread.currentThread();// 获取当前线程ThreadLocalMap map = getMap(t);// ThreadLocalMap 和线程Thread唯一对应,所以 get 操作只有当前线程可以访问if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);// 根据 key 获取 entryif (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;// 取出 value 值return result;}}return setInitialValue();}
小结一下set 和get 流程,里面具体的hash 映射算法和索引计算法未分析,看不懂(线性探测法寻址)
ThreadLocal.set() -> getMap() or createMap() 返回当前 Thread 的 ThreadLocalMap
当前 Thread 的 ThreadLocalMap.put(ThreadLocal, value) 存入数据,其中 key 就是 ThreadLoacal
ThreadLocal.get() -> getMap() 返回当前 Thread 的 ThreadLocalMap
当前 Thread 的 ThreadLocalMap.get(ThreadLocal) 读取数据
ThreadLocal 使用不当会出现内存泄露,出现内存泄露需同时满足以下三个条件:
ThreadLocal 引用被设置为 null ,且后面没有 set,get,remove 操作,该 entry 变成游离状态
线程一直运行,不停止(线程池)
触发了垃圾回收( Minor GC 或 Full GC )
Android Looper 中并没有调用 ThreadLocal 的 remove ,为何不会出现内存泄露呢?主要有以下原因: Looper 中的 ThreadLocal 使用 static final 修饰, static 修饰的生命周期与 Application 同在, Application 退出时线程自然停止运行了,并且 final 修饰其他地方无法修改其引用。因此同时打破了上面的条件1,2,不会出现 ThreadLocalMap 存储数组中 key 为 null 时触发 GC 的内存泄露问题
4 总结一下,由 Handler 引申出来的知识点:
Handler 发送消息流程,与 Looper,MessageQueue 三者之间角色关系
Handler 内存泄露原因以及处理方案
MessageQueue 数据结构,链表和队列区别, Message 入队和出队的遍历方法
Looper 如何保证线程变量唯一, ThreadLocal 原理和内存泄露
ThreadLocal 内存泄露引申出弱引用,软引用和强引用
Message 消息的复用机制,缓存原理,链表结构
Message 设计复用机制的原因,内存抖动,享元设计模式
消息屏障 Barrier 是如何保证优先执行的,以及系统内部应用场景
Android 中应用卡死的定义,ANR
MessageQueue 死循环节约资源处理方案, linux 的 ePoll 机制
IdleHandler 的作用,和处理时机,处理方式






