常见问题
- 子线程为什么不能直接new Handler?
- 子线程真的不能更新UI吗?
- 主线程可以向子线程发送消息吗?
- 子线程中Toast,showDialog,的方法
- Looper死循环为什么不会导致应用卡死?
- Handler的内存泄漏如何避免?
- post方法为什么可以直接在主线程更新UI?
概述
如同四大组件一样,消息机制是Android学习的一道门槛,下面就个人理解进行总结记录一波。Handler是Android消息机制的上层接口,在我们日常使用过程中只需要和Handler交互即可。谈到消息机制,就不得不提Handler、Message、MessageQueue、Looper这四个重要对象,他们合力完成整个消息机制。通常情况下,我们会在主线程创建Handler的实例,开启子线程执行耗时任务,当耗时任务完成以后发送消息给主线程更新UI。其大概流程是Handler发送消息时将消息添加到消息队列MessageQueue(虽然叫队列,实际上是单链表,插入,删除更高效),通过消息轮询器Looper的looper()方法,将消息分发给Handler的handlerMessage方法进行处理。举个例子,就好比写信人把信装入信封,投放到邮筒,邮差小哥从邮桶中取出交给收信人,其角色如下:
- Handler:发件人/收信人 执行具体操作/发送消息
- Message:信封,包装消息的实体
- MessageQueue:邮筒,消息队列,存放消息实体
- Looper:邮差小哥,从消息队列中取出消息,并进行分发
Handler构造方法:
先从Handler构造方法说起,Handler的构造方法有6个,但最终都是调用同一个,代码如下:
public Handler(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());
}
}
//获取当前线程的looper对象
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;
}
不难发现,通过构造方法创建Handler的实例时,会通过myLooper()方法获取当前线程的looper对象,如果为空则会报异常,提示要调用Looper.prepare(),那为什么通常我们在主线程new Handler()时,没报异常呢?是因为在主线程中默认调用了Looper.prepareMainLooper();
Looper
//这个方法创建了一个Looper,并且将它作为主线程的Looper
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
//获取当前线程对应的Looper
sMainLooper = myLooper();
}
}
//获取当前线程的looper
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
在主线程中,也就是ActivityThread的main方法中,实例化了looper,但本质仍然是通过prepare()方法实现的,接下来看看prepare()方法到底做了什么?有一点需要注意的是,主线程的Looper和子线程的Looper细微区别,通过对比就可以发现,调用prepare时方法的参数不同,主线程创建Looper时prepare方法参数为false,表示Looper不可以退出,这很好理解,因为Android系统就是一个无限循环的消息机制,如果Looper退出,系统就无法正常运行了。而子线程的创建Looper时,prepare方法参数为true,表示子线程的Looper可以退出。
public static void prepare() {
prepare(true);
}
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));
}
可以看到,prepare首先会判断当前线程是否已经创建过Looper,若是创建过则会抛出异常信息。也就是说,一个线程只能对应一个Looper,也就是只能prepare一次。若是没有创建过,则会创建一个Looper放到ThreadLocal中。而ThreadLocal是按照线程信息储存数据的,获取时也是根据当前线程获取对应的数据。
为什么不能直接在子线程中new Handler呢?
因为主线程默认调用了Looper.prepare()和Looper.loop()方法,实例化了创建handler的必要条件looper对象。这样我们就可以直接在主线程通过new的方式创建Hanler的实例,在子线程给主线程发送消息。而子线程没有默认调用prepare方法,实例化Looper,故不能直接new Handler。Handler是不同线程之间通信的桥梁,并不局限于子线程给主线程发送消息,只要我们手动调用Looper.prepare()和Looper.loop()方法,就可以在子线程实例化Handler,并在主线程给子线程发送消息,甚至是子线程给子线程发送消息。
//Looper的构造方法
private Looper(boolean quitAllowed) {
//实例化消息队列MessageQueue
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
当调用了prepare创建Looper之后,在ThreadLocal中就会有一个当前线程的Looper对象了。而在Looper的构造方法中,还实例化了一个消息队列MessageQueue,也就是说,每个Looper都对应的有一个消息队列(每个送信人即邮差小哥负责一个邮筒)。
现在已经有了邮筒了,那么邮差小哥什么时候知道有信封被放在了邮筒中呢?当然是时刻守着邮筒啊,也就是Looper将会一直关注着MessageQueue,这在代码中是通过loop方法的一个死循环实现的。
public static void loop() {
//获取当前线程的looper
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//获取looper对应的MessageQueue
final MessageQueue queue = me.mQueue;
...
for (;;) {
//从MessageQueue中取消息,队列中没有消息时,会阻塞在这里
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
try {
//分发消息,取出消息以后调用 handler 的 dispatchMessage() 方法来处理消息
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
msg.recycleUnchecked();
}
}
Looper 中的 loop()方法, 它的作用就是从消息队列MessageQueue 中不断地取消息, 然后将事件分发出去,最终调用的是msg.target.dispatchMessage(msg)来处理消息。那么问题来了主线程中loop()方法的死循环为什么不会造成应用的卡顿呢?
原因如下:
1.耗时操作本身并不会导致主线程卡死, 导致主线程卡死的真正原因是耗时操作之后的触屏操作, 没有在规定的时间内被分发。
2.Looper 中的 loop()方法, 他的作用就是从消息队列MessageQueue 中不断地取消息, 然后将消息分发出去。
补充:网上有很多博主说原因是因为 linux 内核的 epoll 模型, native 层会通过读写文件的方式来通知我们的主线程, 如果有事件就唤醒主线程, 如果没有就让主线程睡眠。 这种说法的正确性有待商榷, 如果说没有事件让主线程休眠是不会造成主线程卡死的原因, 那么有事件的时候, 在忙碌的时候不也是在死循环吗??那为什么忙碌的时候没有卡死呢?这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源
这不是本文重点,欲知详细分析请点击看原文分析
原文为什么Looper中的Loop()方法不能导致主线程卡死?
ThreadLocal
前面提到了Looper对象的存取是通过ThreadLocal实现的,一个线程只会有一个looper对象,那ThreadLocal是如何做到的呢?
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获得存储到的数据,对于其它线程来说则无法获取数据。原因是不同线程访问同一个ThreadLocal的get方法,ThreadLocal内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找对应的value值。很显然,不同线程中的数组是不同的,这就是为什么通过ThreadLocal可以在不同的线程中维护一套数据的副本并且彼此互不干扰。
ThreadLocal的set()源码分析:
//ThreadLocal.java
public void set(T value) {
//1. 获取当前线程
Thread t = Thread.currentThread();
//2. 获取当前线程对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//3. map存在就进行存储
map.set(this, value);
else
//4. 不存在就创建map并且存储
createMap(t, value);
}
//ThreadLocal.java内部类: ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
//1. table为Entry数组
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//2. 根据当前ThreadLocal获取到Hash key,并以此从table中查询出Entry
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//3. 如果Entry的ThreadLocal与当前的ThreadLocal相同,则用新值覆盖e的value
if (k == key) {
e.value = value;
return;
}
//4. Entry没有ThreadLocal则把当前ThreadLocal置入,并存储value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//5. 没有查询到Entry,则新建Entry并且存储value
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//ThreadLocal内部类ThreadLocalMap的静态内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal的get()源码分析:
public T get() {
//1. 获取当前线程对应的ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//2. 取出map中的对应该ThreadLocal的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//3. 获取到entry后返回其中的value
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//4. 没有ThreadLocalMap或者没有获取到ThreadLocal对应的Entry,返回规定数值
return setInitialValue();
}
private T setInitialValue() {
//1. value = null
T value = initialValue();//返回null
Thread t = Thread.currentThread();
//2. 若不存在则新ThreadLocalMap, 在里面以threadlocal为key,value为值,存入entry
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
- 当前线程对应了一个ThreadLocalMap
- 当前线程的ThreadLocal对应一个Map中的Entry(存在table中)
- Entry中key会获取其对应的ThreadLocal, value就是存储的数值
MessageQueue
消息队列扮演着邮筒的角色,主要包含两个操作:插入和读取。读取操作本身伴随着删除操作,插入和读取对应的方法 分别为enqueueMessage和next,其中enqueueMessage的作用是往消息队列中插入一条消息,而next的作用是从消息队列中取出一条消息并将其从消息队列中移除。尽管MessageQueue叫消息队列,但是它的内部实现并不是队列,而是通过一条单链表的数据结构来维护消息列表,单链表在插入和删除上比较有优势。
enqueueMessage的源码分析:
boolean enqueueMessage(Message msg, long when) {
//这里的msg.target指的就是发送该条消息的handler
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) {
......
msg.markInUse();
msg.when = when;
Message p = mMessages;//消息队列头部结点
boolean needWake;
//p == null 表示当前消息队列为空;
//when == 0 表示新消息立即执行;
//when < p.when 表示新消息触发时间比消息队列中头部结点消息触发时间早
//满足三者之一,则将新的消息置为头部结点,也就是插入链表的最前面,可以理解为把刚进入消息队列的消息
//置为消息队列的头部结点
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 {
//插入消息到链表的中间:如果插入消息到链表头部的条件不具备,则遍历
//消息链表比较触发时间的早晚,然后将消息插入到消息链表的合适位置。接着
//如果需要唤醒线程处理则调用C++中的nativeWake()函数.
// 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;
//遍历MessageQueue,如果新消息触发时间早则跳出循环,并将新消息msg插入到p的前面
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;
}
它的主要操作就是单链表的插入操作,往MessageQueue中存放消息。具体分析见代码中注释。
接下来是next方法:
Message next() {
final long ptr = mPtr;
if (ptr == 0) { //当消息循环已经退出,则直接返回
return null;
}
int pendingIdleHandlerCount = -1; // 循环迭代的首次为-1
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//阻塞操作,当等待nextPollTimeoutMillis时长,或者消息队列被唤醒,都会返回
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
//当消息Handler为空时,查询MessageQueue中的下一条异步消息msg,则退出循环。
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
//当异步消息触发时间大于当前时间,则设置下一次轮询的超时时长
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 获取一条消息,并返回
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
//设置消息的使用状态,即flags |= FLAG_IN_USE
msg.markInUse();
return msg; //成功地获取MessageQueue中的下一条即将要执行的消息
}
} else {
//没有消息
nextPollTimeoutMillis = -1;
}
//消息正在退出,返回null
if (mQuitting) {
dispose();
return null;
}
//当消息队列为空,或者是消息队列的第一个消息时
if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
//没有idle handlers 需要运行,则循环并等待。
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
//只有第一次循环时,会运行idle handlers,执行完成后,重置pendingIdleHandlerCount为0.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; //去掉handler的引用
boolean keep = false;
try {
keep = idler.queueIdle(); //idle时执行的方法
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
//重置idle handler个数为0,以保证不会再次重复运行
pendingIdleHandlerCount = 0;
//当调用一个空闲handler时,一个新message能够被分发,因此无需等待可以直接查询pending message.
nextPollTimeoutMillis = 0;
}
}
nativePollOnce是阻塞操作,其中nextPollTimeoutMillis代表下一个消息到来前,还需要等待的时长;当nextPollTimeoutMillis = -1时,表示消息队列中无消息,会一直等待下去。当处于空闲时,往往会执行IdleHandler中的方法。当nativePollOnce()返回后,next()从mMessages中提取一个消息。next()方法是一个无线循环的方法,如果消息队列中没有消息,那么next()方法会一直阻塞,当有新消息到来时,next会将这条消息返回同时也将这条消息从链表中删除。
接下来是从sendMessage方法或者是post方法到handleMessage,从源码角度分析整个消息机制流程,当我们发送消息时,无论采用sendMessage还是post,最总调用的sendMessageAtTime方法,其过程如下:
首先看sendMessage方法:
public final boolean sendMessage(Message msg)
{
Log.d(TAG, "sendMessage: " + Thread.currentThread().getName());
return sendMessageDelayed(msg, 0);
}
调用sendMessageDelayed:
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
再调用sendMessageAtTime:
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);
}
再看看post方法:
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);//getPostMessage方法是两种发送消息的不同之处
}
方法只有一句,内部实现和普通的sendMessage是一样的,但是只有一点不同,那就是 getPostMessage这个方法:
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
这个方法我们发现也是将我们传入的参数Runnable封装成了一个消息,只是这次是m.callback = r,刚才是msg.what=what,至于Message的这些属性就不看了,有兴趣可自行查看。
刚才我们发现两个方法后面都是调用了sendMessageAtTime:
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);
}
这个方法又调用了 enqueueMessage,看名字应该是把消息加入队列的意思,点进去看下:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
mAsynchronous这个异步有关的先不管,继续将参数传给了queue的enqueueMessage方法,至于那个msg的target的赋值,就是将Message的成员target(也就是Handler)赋值为我们实例化的handler,最总分发消息时,就是交给我们实例化的handler进行处理。现在继续进入MessageQueue类的enqueueMessage方法,前面已经分析过了,它的主要操作就是单链表的插入操作,往MessageQueue中存放消息。但是有放就有拿,这个消息怎样把它取出来呢?就是MessageQueue的 next()方法,前面已分析过,我们知道它是用来把消息取出来的就行了。不过这个方法是在什么地方调用的呢,不是在Handler中,我们找到了Looper这个关键人物,我叫他轮询器,专门负责从消息队列中拿消息,关键代码如下:
for (;;) {
Message msg = queue.next(); // might block
...
msg.target.dispatchMessage(msg);
...
msg.recycleUnchecked();
}
简单明了,我们看到了我们刚才说的msg.target,刚才在Handler中赋值了msg.target=this,所以我们来看Handler中的dispatchMessage:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {//callback在message的构造方法中初始化或者使用
//handler.post(Runnable)时候才不为空
handleCallback(msg);
} else {
if (mCallback != null) {//mCallback是一个Callback对象,通过无参的构造
//方法创建出来的handler,该属性为null,此段不执行
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);//最终执行handleMessage方法
}
}
- msg的callback不为空,即post的Runnable不为空,调用handleCallback方法(message.callback.run())//优先级最高
- mCallback不为空,调用mCallback.handleMessage(msg) //优先级居中
- 最后如果其他都为空,执行Handler自身的 handleMessage(msg) 方法//优先级最低
再看看第一种情况下的handleCallback方法
Handler.java
private static void handleCallback(Message message) {
//Message的成员变量Runnable callback;
message.callback.run();
}
为什么在post中传了个Runnable还是在主线程中可以更新UI?
如果有耐心看到这里,相信你应该明白了msg的callback应该已经想到是什么了,就是我们通过Handler.post(Runnable r)传入的Runnable的run方法,这里就要提提java基础了,直接调用线程的run方法相当于是在一个普通的类调用方法,还是在当前线程执行,并不会开启新的线程。只有执行了start方法时,才会开启新的线程。所以到了这里,我们解决了为什么在post中传了个Runnable还是在主线程中可以更新UI的问题。
未完待续…