Android系统源码分析--消息循环机制

上一章我们讲解SystemServer时涉及到了消息机制,因此这一章我们先介绍一下消息循环机制,帮助大家弄清楚消息循环的原理,有助于代码的编写和优化。

Looper-Message-MessageQueue-Handler消息处理机制

在Android系统有两个通信机制,一个是Binder,一个是消息机制,前者是跨进程通信,后者是进程内部通信。消息通信主要包括几个部分:

  • 消息发送者和处理者:Handler
  • 消息循环器:Looper
  • 消息队列:MessageQueue
  • 消息:Message

我们先看一个时序图:

android

图中,1-11步是Looper的准备过程,12-17步是获取消息,处理消息,回收消息的循环过程。

下面是一张消息循环过程图,图片来自网络博客(blog.mindorks.com),Looper会通过loop方法不断从消息队列去取消息,然后交给handler处理,处理完成就回收消息,要注意的是只有一个looper,但是可能有多个handler:

android

1、Looper

Looper是一个循环器,通过里面的loop方法不断去取消息,发送给Handler进行处理。根据上面时序图以及SystemServer启动代码我们开始分析Looper的调用过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void run() {
try {
...
// 准备主线程的Looper
Looper.prepareMainLooper();
...
} finally {
...
}
...
// Loop(循环) forever.
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}

我们先看Looper.prepareMainLooper方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Initialize the current thread as a looper, marking it as an
* application's main looper. The main looper for your application
* is created by the Android environment, so you should never need
* to call this function yourself. See also: {@link #prepare()}
*/
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}

上面有段注释,我翻译一下,就是:初始化当前线程作为一个looper,并把它标记为应用的主looper。这个looper是被Android环境(系统)创建的,因此你不需要自己调用这个方法。也就是系统创建了这个looper,你不需要再创建了。我们接着看里面的内容,首先调用了prepare方法,需要注意的是Looper.prepare()在每个线程只允许执行一次,该方法会创建Looper对象:

1
2
3
4
5
6
7
8
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
...
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {// 确保ThreadLocal中只有一个Looper
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

上面的ThreadLocal是声明在类里的,并且是静态的,因此,随着类创建了该对象,get方法是获取Looper的,如果能获取到,则抛出异常,也就是确保当前线程只有一个Looper。如果是空,那么我们创建一个Looper放到里面去。

我们先看一下ThreadLocal:线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。(来自Android消息机制1-Handler(Java层))我们看一下它的set和get方法:

ThreadLocal的set方法:

1
2
3
4
5
6
7
8
9
10
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程里的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

如果map不为空,则以键值对放入进行存储,此处map不是HashMap,而是其他,这里不详细解释。如果map为空,则通过下面代码创建map:

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal的get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

我们看到获取的时候也是根据当前线程去获取的。因此每个线程会保存一个Looper。

我们接着看Looper的构造函数有哪些操作,也就是创建Looper做了哪些处理:

1
2
3
4
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

首先是创建了MessageQueue对象,接着创建一个线程,也就是当前线程,currentThread是一个native方法,我们不再分析,我们看一下MessageQueue创建做了哪些事情:

1
2
3
4
5
6
MessageQueue(boolean quitAllowed) {
// 是否可以退出消息队列
mQuitAllowed = quitAllowed;
// 返回底层的MessageQueue对象的内存地址,如果为空返回0
mPtr = nativeInit();
}

上面的nativeInit是调用的jni,我贴一下代码,不再解释:

android

我们回到prepareMainLooper方法接着看,如果sMainLooper不为null,则抛出异常,提示sMainLooper已经创建了,如果是null,那么调用myLooper方法回去sMainLooper:

1
2
3
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}

其实这个get方法就是我们上面new完Looper放进去的,到此prepareMainLooper就完成了,相关信息也准备好了。接下来就是调用Looper.loop方法,方法下面是一个异常,怎么样才能保证异常不会抛出,就是loop方法永远执行不完。是不是只有我们接着看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
...
for (;;) {// 无限循环
Message msg = queue.next(); // might block
if (msg == null) { // message为空为结束信号,退出循环
// No message indicates that the message queue is quitting.
return;
}
...
try {
// 将真正的处理工作交给message的target,即handler
msg.target.dispatchMessage(msg);
} finally {
...
}
...
// 回收Message
msg.recycleUnchecked();
}
}

首先是通过myLooper方法获取Looper,如果为空,则抛出异常,提示还没有调用Looper.prepare方法,如果不为空,则通过looper获取MessageQueue对象,然后进入for循环,因为for语句中没有条件,因此该for循环为无限循环,在这个循环中有三件事,一个是获取消息队列中的下一个消息,然后处理该消息,最后处理完消息,回收消息。这三个过程就是Looper的主要作用:取消息,处理消息,回收消息。

2.Message

Message是整个循环中信息的载体,它是一个链表结构,关于链表结构可以参考下面文章:
Android自助餐–Handler消息机制完全解析–系列
链表数据结构图解 和 代码实现
基本数据结构:链表(list)
链表结构之单链表

我们看个图:

android

上面就是一个示例图,每个Message中都有一个后面Message的引用next,链表最后一个next为空,sPool是第一个Message。但是每个Message的内存地址不是挨着的,这样可以占用零碎的内存。

我们先来看Message包含的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int what;
public int arg1;
public int arg2;
public Object obj;
/*package*/ int flags;
/*package*/ long when;
/*package*/ Handler target;
// 消息队列中下一个消息的引用
/*package*/ Message next;
// sPool这个变量可以理解为消息队列的头部的指针,也就是当前消息对象
private static Message sPool;
// sPoolSize是当前的消息队列的长度
private static int sPoolSize = 0;
private static final int MAX_POOL_SIZE = 50;

前四个参数很熟悉,不再解释,flags是一个标签,表示是否正在使用;when是处理消息的时间;target就是我们上面提到的Handler;next是下一个Message的引用;sPool是一个静态变量,说明只有一个,其实这个是消息队列的头消息;sPoolSize是消息队列中消息个数;MAX_POOL_SIZE是消息队列最大消息数量。

Message中有多个用来获取Message对象的obtain复写方法。因为后面的obtain方法都是通过第一个obtain方法获取Message对象的,因此我们只看第一个参数为空的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*/
public static Message obtain() {
// 避免多线程进行争抢资源,给sPoolSync进行加锁
synchronized (sPoolSync) {
// 如果消息队列的头部不为空,则可以取出头部重用
if (sPool != null) {
Message m = sPool;
// 头部消息取出后,将sPool指向后面的消息对象
sPool = m.next;
// next(队列尾部)设置为null
m.next = null;
m.flags = 0; // clear in-use flag
// 消息队列长度减一
sPoolSize--;
return m;
}
}
// 如果消息队列的头部为空,则创建新的Message对象
return new Message();
}

系统提示尽量用这种方法获取Message对象,避免创建大量新的对象,其实也可以通过Handler来获取Message,这个我们在将Handler时候再讲。

在上面Looper中我们讲到最后消息处理完后需要回收,这个回收方法recycleUnchecked也在Message类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Recycles a Message that may be in-use.
* Used internally by the MessageQueue and Looper when disposing of queued Messages.
*/
void 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 = -1;
when = 0;
target = null;
callback = null;
data = null;
// 避免多线程进行争抢资源,给sPoolSync进行加锁
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
// 回收当前消息后时,将sPool消息后移
next = sPool;
// 将当前消息放到头部
sPool = this;
// 队列长度加一
sPoolSize++;
}
}
}

消息回收时,将对应消息的标签设置为使用中,其他标签设置为空或者默认值,如果消息队列没有超过最大值,那么将sPool赋值给next,将这个Message赋值给sPool,消息队列长度加一。也就是将处理完的消息清空,重新放回消息队列等待使用。

3.Handler

Handler是发送消息和处理消息的工具。我们先看构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
public Handler(Callback callback, boolean async) {
...
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}

Handler中的looper是获取当前线程中的looper,looper不能为空,MessageQueue也是looper中的。

首先是发送消息,发送消息的方法很多,我们看一张图:

android

我们看到Handler中有多个发送消息的方法,但是最终调用了enqueueMessage方法:

1
2
3
4
5
6
7
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

从代码我们可以看到msg.target就是Handler,也就是在这里进行赋值的,然后是调用MQ(MessageQueue)的enqueueMessage方法,这个方法是添加消息队列的,具体内容我们后面再讲。因此,发送消息就是将消息添加到消息队列。我们前面还讲过获取Message对象可以通过Message中的obtain方法,也可以通过Handler中的方法,我们先看一张图:

android

Handler是通过多个复写方法obtainMessage来获取Message的,只是传入参数不同,我们看一个没有参数的方法代码:

1
2
3
public final Message obtainMessage(){
return Message.obtain(this);
}

我们看到其实还是调用了Message.obtain方法,并且传入了this,也就是Handler,通过Message.obtain方法将Handler赋值给Message中的target。从这,我们基本对Handler与Message的关系基本明确了,获取Message的方法我们也完全知道了,因此我们在以后用的时候不需要再去new一个Message对象,而是通过obtain方法去获取,如果有就不需要new了,如果没有系统会自己去创建。

4.MessageQueue

MessageQueue是消息队列,其实是管理消息链表的。它主要功能是取出消息–next方法,将消息加入队列–enqueueMessage方法。

我们先看加入消息队列方法enqueueMessage,也就是Handler中发送消息后加入队列的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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.");
}
synchronized (this) {
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;
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;
}

插入消息队列有两种情况,一种是消息队列处于空闲状态,直接将消息放在消息队列前面,可能需要唤醒主线程,另一种是消息队列处于忙碌状态,就不需要唤醒,而是根据消息处理时间将消息插入到消息队列的对应位置中。

第一种状态:插入队列头

1
2
3
4
5
6
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
}

if语句的三个条件是:一、队列为空,二、插入消息需要立即处理,三、插入消息处理时间比消息队列头消息早,这三个条件说明消息队列处于闲置状态,此时要把消息放置到消息队列头部,即将插入消息的next指向消息队列的头p,然后将消息队列要处理的消息指向插入消息对象,最后判断是否需要唤醒,如果队列阻塞则需要唤醒,否则不需要。

第二种状态:插入队列中间或者后面,这种情况比较复杂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

因为不是在队列头,所以需要for循环去查找应该的位置,首先将第一个消息用prev进行缓存,然后当前消息引用指向下一个消息对象,依次类推,直到p == null(到队列最后),或者当前消息触发时间小于后面这个消息的触发时间,停止循环,说明找到了位置,此时执行最后两行代码,也就是将当前出入消息的next指向p,也就是,如果p==null,则说明插入到最后一个,如果不为空,则插入到p前面,然后将前一个prev的next指向插入的消息,此时插入成功。最后的if语句中如果需要唤醒消息队列,则调用底层方法nativeWake唤醒消息队列开始循环。到此,消息插入就讲完了。我们上面说到loop方法是通过MessageQueue的next方法取出消息,那么下面我们看一下next方法是怎么取出消息的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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);
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) {
// 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;
}
}

mPtr是MessageQueue初始化的时候通过调用底层方法获取的底层NativeMessageQueue的对象,如果底层不能初始化则返回0,如果可以初始化返回对象地址,此处判断,如果没有初始化也就没有底层的NativeMessageQueue对象,因此返回null。紧接着开始for循环,开始遍历消息队列,查找需要处理的消息,在这里,如果消息队列为空,或者没有需要立即处理的消息都要使线程开始等待。接着调用nativePollOnce方法来查看当前队列中有没有消息,传入参数nextPollTimeoutMillis表示要等待的时间,如果nextPollTimeoutMillis为0则说明不需要等待。接着获取当前时间now,初始化prevMsg来缓存消息,初始化msg来缓存当前消息mMessages,下面if语句判断消息不为空但target为空,则说明该消息为“同步分隔栏”(关于“同步分隔栏”请参看聊一聊Android的消息机制一文),如果该消息为同步分隔栏,则后面的同步消息都不会被查找,只能查找异步消息来处理,也就是do-while语句中的代码,如果没有同步分割栏或者找到了后面的异步消息(可能没有),则接着判断。

如果消息不为空,则还有消息,开始判断时间,如果当前时间小于下一个消息的执行时间,说明还需要等待,那么计算需要等待的时间nextPollTimeoutMillis,如果当前时间不小于当前消息执行时间时,并且前一个消息prevMsg不为空,说明出现了“同步分隔栏”,也就是执行了do-while代码,do-while执行完,说明找到了异步消息或者遍历完整个队列没有异步消息,如果有异步消息,此时prevMsg.next = msg.next,也就是跳过同步消息,将异步消息msg.next赋值给prevMsg.next,然后将取出的msg的next赋值为null,因为要处理了,所以不再指向后面队列的消息对象,然后将msg设置为正在使用,并且返回,如果prevMsg为空,则说明没有出现“同步分隔栏”,此时将当前消息mMessages的下一个消息赋值给mMessages,然后将msg.next设置为空,就是不再引用,然后设置为正在使用,返回该消息。

如果消息为空,则nextPollTimeoutMillis = -1,说明没有消息了,则接着向下执行,如果退出消息队列,则说明所有消息都执行完了,最终调用nativeDestroy方法,如果不退出消息队列,则要进入等待状态。如果第一次进入,并且当前消息为空或者消息不为空,但是处于等待状态,那么要获取IdleHandler个数,如果小于等于0,则说明没有IdleHandler运行,调用continue执行下一次循环,如果IdleHandler个数大于0,但是等待的Handler(mPendingIdleHandlers)为空,则要创建IdleHandler数组,将mIdleHandlers放入数据,然后for循环调用每个IdleHandler的queueIdle方法,如果这个方法返回false,则从数组移除这个对象,否则保留改对象,下次空闲继续执行,最后将pendingIdleHandlerCount置为0,nextPollTimeoutMillis置为0,继续下一次循环。

那么到此,整个循环就讲完了,因为不懂C++代码,所以底层没法分析,只能分析framework层代码,说了很多还是需要自己对比代码多理解。

5.Handler的使用方法

我们在使用Handler的时候软件会提示我们有问题,那么到底该怎么写Handler呢,我从Stack Overflow找到了答案,在这就分享一下:

首先,定义一个静态MxHandler继承Handler,里面使用弱引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class MxHandler<T> extends Handler {
private WeakReference<T> weak;
public MxHandler(T t) {
this.weak = new WeakReference<T>(t);
}
@Override
public void handleMessage(Message msg) {
if (null == weak || null == weak.get()) {
return;
}
handleMessage(msg, weak);
super.handleMessage(msg);
}
protected abstract void handleMessage(Message msg, WeakReference<T> weak);
}

然后我们再写具体的MyHandler继承这个MxHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static final class MyHandler extends MxHandler<HandlerDemo> {
public MyHandler(HandlerDemo handlerDemo) {
super(handlerDemo);
}
@Override
protected void handleMessage(Message msg, WeakReference<HandlerDemo> weak) {
switch (msg.what) {
case 0:
HandlerDemo h = weak.get();
h.doSomething();
break;
default:
break;
}
}
}

这样我们在Activity中使用是不会出现内存泄漏之类的错误。

代码地址:

直接拉取导入开发工具(Intellij idea或者Android studio)

https://git.coding.net/codemx/Android-25.git

参考:

android的消息处理机制(图+源码分析)——Looper,Handler,Message
Android中Thread、Handler、Looper、MessageQueue的原理分析
Android 异步消息处理机制 让你深入理解 Looper、Handler、Message三者关系
Android应用程序消息处理机制(Looper、Handler)分析

Android开发群:192508518

微信公众账号:Code-MX

注:本文原创,转载请注明出处,多谢。

×

纯属好玩

扫码支持
扫码打赏,多少你说算

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. Looper-Message-MessageQueue-Handler消息处理机制
    1. 1.1. 1、Looper
    2. 1.2. 2.Message
    3. 1.3. 3.Handler
    4. 1.4. 4.MessageQueue
    5. 1.5. 5.Handler的使用方法
  2. 2. 代码地址:
  3. 3. 参考:
  4. 4.
,