前言
上一篇Handler 使用以及源码全面解析(一)
简述了Handler的使用之后,这篇从源码上来分析Handler的原理。
分析源码时我们可以发现很多我们未曾注意到的小细节。
比如我们都知道,每个使用Handler的线程都要有自己Looper, 而使用Looper.myLooper()就可以得到当前线程的Looper. 怎么这么神奇呢? 那么每个线程各自的Looper是如何管理的呢?
// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal sThreadLocal = new ThreadLocal();
private 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));
}
/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
从源码可以看到,用的是ThreadLocal来保存的,通过ThreadLocal.get()就获得当前线程Looper对象。所以进入Handler的正题之前,不妨先来了解下What is ThreadLoacl
。
1、ThreadLocal
每个线程都可能需要有自己的数据副本,比如有的线程需要Looper,有的不需要,用脚指头想一下,Looper是不可能作为成员变量定义在Thread中。而这种一一对应的映射关系,很自然而然我(们)想到了map的形式----Map
,通用点就用上泛型,Map
是的,ThreadLocal内部大致是Map这种形式,但并不是Map
Thread虽然没有Looper变量,但是它持有ThreadLocalMap
变量啊!Map中的每个映射关系是这样的---Entry
,即Thread拥有y由很多个Entry
而通过ThreadLocal所谓能存取不同线程的副本的原因,正是每次进行get和set方法时,ThreadLocal都会用Thread.currentThread()来获取当前线程ThreadLocalMap,把该ThreadLocal作为key,从ThreadLocalMap进行相应的存取Value操作
ThreadLocal.java
public T get() {
Thread t = Thread.currentThread();
//Thread中的threadLocals变量
ThreadLocalMap map = getMap(t); //return t.threadLocals;
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 可以实现initialValue()方法去设置初始值
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
这个ThreadLocalMap还有一点不同的是,并不是我们常见的HashMap。HashMap的setValue原理,我们都知道,Key的hash值对数组长度取余就得到Key在数组的位置,若该位置已经有其它Key存在,那就会生成一个指针,与相同位置的形成一条单向直链,链长度过长时还会转化为红黑树的结构(java 1.8)。
而ThreadLocalMap并不不存在指针,也就没有链。在取余得到的位置被占有后,则会从该位置起对数组进行遍历,知道找到空位为止。
当然我们一般情况下我们也不会去使用ThreadLocal,当某些数据在以线程为作用域且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。可能读者至今都没有用过(哈哈,我在业务需求上也没有用过~)
再举一个例子,AMS中的使用:
//记录不同线程的pid和uid
private class Identity {
public final IBinder token;
public final int pid;
public final int uid;
Identity(IBinder _token, int _pid, int _uid) {
token = _token;
pid = _pid;
uid = _uid;
}
}
private static final ThreadLocal sCallerIdentity = new ThreadLocal();
再多说一句,Android 5.0到7.0之间,Android SDK 中ThreadLocal的代码跟JDK是不一致的,是所谓优化过的,同样用数组,偶数下标存Key,奇数下标存Value
values[index] = key //(ThreadLocal>.refrence,弱引用)
values[index+1] = value//()
7.0及之后的就保持一致了,真香。
接下里进入正题了,那接下来我可要放大招了 要贴源码了。
Looper是某个餐厅(Thread)的专职外卖小哥,Handler是某个点外卖的宅男。
MessageQueue相当于一个线上下单的餐厅,会根据顾客需求把所有订单按出餐的时间排序(为什么不是下单时间,是因为有些顾客会让餐厅定时延后送过来)。
开始赏析外卖小哥与宅男之间的大戏吧
2、Looper
Looper 是用于为线程提供一个消息循环的机制。
线程默认不提供Looper,而主线程是在应用初始化的时候就为主线程提供了Looper. 证据何在呢?
餐厅默认不提供外卖,也就没有招聘外卖小哥。
当系统启动一个应用时,入口函数即ActivityThread.main(String[] args)。
ActivityThread.java
public static void main(String[] args) {
...
Looper.prepareMainLooper();//接入外卖系统,招聘外卖小哥。
...
Looper.loop(); // 系统运转
throw new RuntimeException("Main thread loop unexpectedly exited");
}
真像大白,啧啧。
主线程开了家餐厅,就同时做了外卖。率先吃了螃蟹,成为第一批富起来的资本家。
looper初始化:开餐厅的准备工作
创建主线程的Looper
public static void prepareMainLooper() {
prepare(false); //
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
// 私有方法,参数为,是否允许该线程退出loop;公有方法prepare()默认true。
// 这个参数会传给MessageQueue
private 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));
}
Looper.loop(); // 。外卖系统开启。
进入循环,loop() :餐厅外卖系统开始运转
通过for(;;)循环的MessageQueue.next()读取下一条消息,将消息分配给对应的handler执行。
读取下一条消息可能会发生阻塞,一是消息队列没有消息,等待下一个消息入列;二是消息还未到执行的时间(消息延迟执行。)
外卖小哥送外一单外卖回来就继续干下一单,取单时可能出现,当前没人点单,或者还没到送货时间,于是就做家门口抽起了烟,等待资本家跟他说可以开始配送了。
// 源码太长, 删除一些,是系统关于每条消息执行时间的一些log以及分析。
public static void loop() {
for(;;) {
Message msg = queue.next();
if (msg == null) { // 返回的消息为空,说明线程退出looper。
// No message indicates that the message queue is quitting.
return;
}
msg.target.dispatchMessage(msg); // 配送
}
}
for(;;)
无限的取订单,并且派送,直到老板明确告诉今天关门了不接单了。
退出循环:餐厅今日休业
休业分两种,第一种直接关闭外卖系统。第二种把超时的订单送完再直接关闭外卖系统。
public void quit() {
mQueue.quit(false);
}
public void quitSafely() {
mQueue.quit(true);
}
直接调用MessageQueue.quit(boolean) 方法,false,即删除队列中所有消息,
true则对比消息队列中when与当前的时间,msg.when小于当前时间的依旧会被保留并执行。比如:
handler.sendEmptyMessage(A)
handler.sendEmptyMessageDelay(B,1000)
looper.quitSafely()
则A会被执行,B则被删除。
3、Handler
发消息:下订单
Handler中发消息有很多种方式。但殊途同归,最终会走到sendMessageAtTime(msg,upTimeMillis)
//uptimeMillis 消息应当被执行的时间。用于消息的排序。
public boolean sendMessageAtTime(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(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this; // 记住 target
if (mAsynchronous) { // 让我们也先记住这个变量。vip订单。稍后再分析
msg.setAsynchronous(true);
}
// 进入消息队列。待分析MessageQueue再看
return queue.enqueueMessage(msg, uptimeMillis);
}
也就是每一个宅男腐女在同一家餐厅下的单,都会有下单时间或者定时送餐时间这个时间属性
收消息: 配送过程
当每一条消息从消息队列取出来要被执行时,msg.target.dispatchMessage(msg)
, 上面的enqueueMessage()这个方法已经告诉我们target就是相对应的Handler!
也就是系统是知道每个订单是对应哪一个宅男的.然后根据指定的配送方式开始配送.
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) { // ①
handleCallback(msg); // 也就是执行 message.callback.run();
} else {
if (mCallback != null) { // ② 怎么又有个CallBack...
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
聊一下这两个CallBack吧。
第一个msg.callBack其实就是下面这种:
handler.post(Runnable {
//本质依然是Message, Message还有个 Runnable变量,系统直接执行Runnable中代码,无需设置what之类的
})
相当于:
val msg = Message.obtain(handler, Runnable {...})
handler.sendMessage(msg)
msg.callBack 就是个 Runnable
handler.mCallBack 就不一样了。是Handler中定义的接口。
public interface Callback {
/**
* @param msg A {@link android.os.Message Message} object
* @return True if no further handling is desired
*/
public boolean handleMessage(Message msg);
}
我们知道实例化Handler的普通方式是,Handler() 或者 Handler(Looper),然后实现handlerMessage()方法。
而实际上,在这两种方法基础上还可以添加 CallBack参数,比如说、
Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
//do sth by Message ...
return false;
}
});
这里的CallBack.handlerMessage就会优先于Handler.handlerMessage()执行。
如果CallBack.handlerMessage 返回true,则不再执行Handler.handlerMessage()
说到构造函数,这里回头来说下mAsynchronous这个变量。年少健忘的你,赶紧往上翻一下。
这个变量其实也是Handler构造函数的一个参数,默认为false,用于表示这个Handler发出的消息是否为异步消息(true为异步),影响MessageQueue在出队时的顺序。
public Handler() {
this(null, false);
}
public Handler(Callback callback, boolean async) {
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
所以我们可以这么理解,收消息,是外卖小哥的配送过程,各种CallBack是宅男指定的配送方式(空运啊,水运或者玛莎拉蒂专送)。
而异步消息,就是宅男是个Vip客户,发出的每条订单都是vip订单,在特殊情况下会有优先配送的特权。
而所谓的特殊情况,稍后分析。
移除消息:取消订单
能移除指定的消息或者全部消息。
Handler实例 A,调用下述的移除消息的方法时,只能移除 msg.target == A 的msg
也就是只能取消自己下的订单。
removeMessages(int what) { // msg.what == what
mQueue.removeMessages(this, what, null); // MessageQueue的逻辑
}
removeMessages(int what, Object object) // msg.what == what && (msg.obj == object|| obj==null)
// token为null,则移除所有(msg.target == 此handler)的msg
removeCallbacksAndMessages(Object token)
4、MessageQueue
其实上述我们可以看到,Looper和Handler的部分逻辑都是在写在MessageQueue里面的。
所以这里如果还提外卖小哥和宅男,就跟上面的描述重复了。我们就看看枯燥的source code吧。
MessageQueue,消息队列。 存有当前线程的所有未执行的消息列表。这个列表以单链表形式存在。
链表的节点的数据结构正是 Message, Message有一个 Message next = null变量用于指向下一个节点。
可通过Looper.myQueue()获取当前线程的MessageQueue实例。
MessageQueue提供了很多方法来对这个消息链表进行操作。挑几个重要的说一说。
也就是出列、入列以及退出循环。
出列
出列,即从消息队列中读取表头的消息。Looper.loop()方法无限循环通过MessageQueue.next()获取下一条消息。
next()内部也有使用了for(;;)的无限循环迭代,直到链表为空的时候会阻塞直到下个消息的入列。
这里先抛出一个疑问,loop()已经是无限循环了,为何这里也需要循环?
Message next() {
...
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
// ①
//JNI 方法,当前线程在nextPollTimeoutMillis 后再唤醒执行
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// ②
if (msg != null && msg.target == null) { // 注意这里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());
}
// ③
if (msg != null) {
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) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
// ④
//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
&& (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;
}
}
这个代码有点长,主要分为四个步骤:
- 当前线程进入阻塞,指定nextPollTimeoutMillis时间之后被唤醒,-1表示一直阻塞(消息队列为空)
- 如果消息列表的表头的target为null,则获取列表中的第一条异步消息。进入步骤三。
- 如果当前消息的执行时间when 大于当前时间,则nextPollTimeoutMilli等于两者的差值,线程可能进入阻塞状态,并进入步骤四。否则next()方法直接返回当前消息。
- 来到步骤四,要么是当前消息列表为空,要么当前要执行的消息还没到时间。也就是属于空闲状态,所以会执行IdleHandlers的任务。这里若IdleHandlers若为空,则进入步骤1,否则执行完IdleHandlers之后进入步骤2.
从执行步骤上来看,在步骤四上,是存在多次迭代甚至一直循环的可能性,所以这里用for(;;)来处理。就一般来说,for(;;)会很快在步骤三就退出循环
这里有两个知识点:IdleHandler 是什么,message.target什么时候回等于null。留着最后讲解。
入列
根据handler.sendMessageXXX()调用的时间以及延迟的时间获得执行的时间,即msg.when,根据when,将message插在链表适当的位置,可能是第一个。
boolean enqueueMessage(Message msg, long when) {
...
// 删除一些判断条件
...
synchronized (this) {
if (mQuitting) { // 执行了 Looper.quit()
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; // mMessage,当前消息链表的头结点
boolean needWake;
if (p == null || when == 0 || when < p.when) { //当前消息插在表头
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 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();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
退出循环
在Looper.quit()我们就提到,其内部实现调用的是 MessageQueue.quit(boolean)
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
// 把
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr);
}
}
5、小结
线程是餐厅,Looper该餐厅的是外卖小哥,Handler是某个点外卖的宅男。
MessageQueue相当于一个线上下单的餐厅,会根据顾客需求把所有订单按出餐的时间排序(为什么不是下单时间,是因为有些顾客会让你定时送过来)。
外卖小哥负责按时把外卖按照指定的配送方式送到宅男手上。则宅男可以下单、指定配送方式以及取消订单。
而IdelHandler就像没人下单,老板很闲,就去接了点私活。
异步消息呢,就像是Vip订单,当出现特殊情况(target
==null),我们称之为会员活动日,当会员活动日到来的时候,外卖小哥就会优先派送Vip订单,除非已经没有Vip订单了或者活动结束了才会派送普通订单。
当老板想停止营业了,可以发出两种指令,一种就是把所有的剩下的订单全部丢掉直接关门,还有一种就是由于订餐量太多,很多超时
没能派送出去的订单,老板会把派完再关门,至于那些还没到时间派送的订单,就只能全部自动退订!
6、异步消息与同步消息:会员活动日
前面我们已经有解释过同步异步消息了,就是在Handle对象初始化时构造参数boolean async
的区别。
会员活动日的到来
一般来说这个参数不会有任何作用,直到会员活动日的到来。
也就是餐厅老板,在门口挂上牌子,MessageQueue.postSyncBarrier()
MessaegQueue.java
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
根据需求在指定的时间when,宣布会员活动的到来。即插入一条target==null
的message!
同时返回一个 token值,即 本次是 第几届 会员活动日。
会员活动日的结束。
如果会员活动一直不结束的话,每送完一单,外卖小哥都是会先看看还有没Vip单,然后优先派送Vip,如果这个订单还没有到时间,那么外卖小哥就会先停下来抽个烟。。直到可以派送Vip单。。这样效率就很低了。所以,需要及时的取消活动日。
public void removeSyncBarrier(int token) {
// Remove a sync barrier token from the queue.
// If the queue is no longer stalled by a barrier then wake it.
synchronized (this) {
Message prev = null;
Message p = mMessages;
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
}
if (p == null) {
throw new IllegalStateException("The specified message queue synchronization "
+ " barrier token has not been posted or has already been removed.");
}
final boolean needWake;
if (prev != null) {
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
p.recycleUnchecked();
// If the loop is quitting then it is already awake.
// We can assume mPtr != 0 when mQuitting is false.
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
根据token 结束对应的该届的活动。
7、IdleHandler :接私活
虽然不在饭点,不用送外卖,作为老板自然要有点上进心,引进点其他业务坐也是提升营收的好办法嘛。比如既然大家平常下订单都喜欢备注多加米饭,趁现在多煮一锅饭嘛。或者直接跟别人三缺一,打个四圈,也是劳逸结合嘛。
在讲述MessageQueue.next()已经提过源码了,再把相关的贴一下吧。
public Message next() {
int pendingIdleHandlerCount = -1;
for(;;) {
...// msg == null
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);
}
}
}
// 设置为0之后,在这个for(;;)里面或者这次的next()的调用中,上面的这个idle的for循环不会再次运行了
pendingIdleHandlerCount = 0;
}
只有在 当 消息队列为空或者 第一条消息还没到执行的时间,IdleHandler才会被执行,跟普通的Message不同的是,每条IdleHandler可能会被执行多次,如果这个IdlerHandler被定义为保留的话-- keep = idler.queueIdle() == true
当然在同一次空闲时间内,Idles只会被执行一次。
那么如何接私活呢?
先看下私活模板长啥样。。
MessageQueue.java
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();
}
私活的内容写在queueIdle()里面,同时用返回值告知老板,这个是不是长期任务。
然后通过订单系统MessageQueue.addIldeHandler()
把私活加入到私活列表上!
MessageQueue.java
/**
* Add a new {@link IdleHandler} to this message queue. This may be
* removed automatically for you by returning false from
* {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is
* invoked, or explicitly removing it with {@link #removeIdleHandler}.
*
* This method is safe to call from any thread.
*
* @param handler The IdleHandler to be added.
*/
public void addIdleHandler(@NonNull IdleHandler handler) {
if (handler == null) {
throw new NullPointerException("Can't add a null IdleHandler");
}
synchronized (this) {
mIdleHandlers.add(handler);
}
}
后记
好啦到这里尾声了。修修改改,偷偷懒懒。我太难了。。找时间再把,同步消息以及异步消息的例子给补上。