前言
由于笔者最近一段时间的工作重心都在大模型和鸿蒙上,Android 代码在平时写的越来越少了。所以突发奇想考虑把 Android 的架构知识整理一遍,也算是一个积累巩固的过程。今天整理一篇 Handler 的文章。
1. 子线程能访问 UI 吗?
这个问题对于 Android 开发来说绝对是老生常谈了吧。子线程肯定是不能直接访问 UI 的。因为 Android 的 UI 框架是单线程模型设计的,所有的 UI 操作都必须在主线程上进行,以确保线程安全和一致性。如果多个线程同时操作 UI 组件,可能会导致数据竞争和不一致的状态。
可能有人会说,并发不安全那就加锁嘛。之所以不加锁是因为加锁机制会让 UI 访问的逻辑变的复杂,降低 UI 的访问效率,因为锁机制会阻塞某些线程的执行。
在 Android 源码中会对 UI 的访问执行线程的检查:
// ViewRootImpl.java
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
看到了吧,只要不是主线程访问 UI,直接就抛异常。虽然子线程中不能直接更新 UI,但是可以利用一些方式,把 UI 更新的操作从子线程切换到主线程,Handler 就是一种方式。
2. 说说 Handler 的原理?
Handler 的原理很简单,主要就是三个角色:
Handler:发送消息到消息队列;MessageQueue: 消息队列;Looper: 从消息队列里面取消息。
看不懂没关系,接下来以子线程向主线程发送消息为例,详细说说。
第一步
子线程中使用 Handler.sendMessage() 方法,准备向主线程发送消息。
// Handler.java
public final boolean sendMessage(@NonNull Message msg) {
return sendMessageDelayed(msg, 0);
}
里面调用了 sendMessageDelayed() 方法,看名字就知道是用来发送延时消息的,只不过这里指定了需要延时的时间为 0,也就是不用延时:
// Handler.java
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
// 当前时间 + 延时时间
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
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);
}
继续看发现调用了 sendMessageAtTime() 方法,在里面会使用 enqueueMessage() 方法将消息放入消息队列 MessageQueue 中。
第二步
在第一步中已经把消息通过 Handler 放入消息队列中了,接下来详细看看消息队列 MessageQueue 是怎么处理传过来的消息的:
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
synchronized (this) {
...
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;
}
...
}
return true;
}
代码有点长,我稍微省略了一些,只看关键代码。首先入参中有一个参数 when,该参数其实是为每个消息都指定一个时间,如果使用 Handler 发送的消息是延时的,那么这个 when 就是“当前时间 + 延时时间”。
为什么需要这个参数 when 呢?从上面的源码中也能看出(标注了关键注释),MessageQueue 其实是一个按照消息时间排序的单链表,加上时间之后可以便于在轮询时比较消息的时间,使其按照时间进行排序。
第三步
前面两步先在子线程中用 Handler 发送消息,然后把消息按照时间进行排序存入消息队列中,接下来该轮到主线程从消息队列中取消息处理了。
取消息的话用到的是 Looper,它会通过执行一个死循环 looper.loop() 不断地从消息队列中取出消息。
这里简单提一下,App 启动时 Zygote 进程在 fork 新进程的时候,会通过反射执行主线程 ActivityThread 的 main 函数,该函数中就会执行到 looper.loop()。所以在 Android 主线程中,会一直通过 Looper 不断地从消息队列中取消息的。(这里不懂也没关系,你只需要知道主线程中会使用 looper 来取消息,关于 App 启动的流程,以后会出文章说明)。
// Looper.java
// loop()中的死循环
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
// 取消息
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}
...
}
上面是进入 loop() 方法的代码,可以看到在死循环中,不断地执行 loopOnce(),而里面的关键就是 mQueue.next()。
第四步
前面说到 loop() 中会使用 mQueue.next() 来取消息:
// MessageQueue.java
Message next() {
...
int nextPollTimeoutMillis = 0;
for (;;) {
...
// 调用 native 函数执行休眠
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) {
// 还没到下个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) {
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;
}
...
}
}
代码很多,我们只看关键信息。前面说过在放入消息时,需要按照时间排序。那么当从消息队列中使用 next() 取出消息时,如果当前的消息还没有到其设置的应该执行的时间,此时就要进入睡眠 nativePollOnce(ptr, nextPollTimeoutMillis);。
从名字能看出,这是一个 native 方法,在底层使用了 epoll 机制来等待(epoll_wait)和检测 I/O 事件。用于在没有消息处理时阻塞线程,并在有新消息或 I/O 事件时唤醒线程。
epoll 是 Linux 内核提供的一种高效的 I/O 事件通知机制,它允许应用程序监视多个文件描述符,以查看是否有 I/O 操作可执行。当使用 epoll 时,应用程序可以在一个或多个文件描述符上等待事件,而不需要像 select 和 poll 一样频繁地轮询它们。
所以如果当前的消息还没有到其设置的应该执行的时间,就进入睡眠,否则就从消息队列中取出消息返回。
第五步
现在我们已经通过 Looper 执行了 messageQueue.next() 从消息队列中取出了消息,接下来就需要对取出的消息进行处理了:
// Looper.java
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
// 取出消息
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}
...
try {
// 处理消息
msg.target.dispatchMessage(msg);
...
} finally {
...
}
...
return true;
}
看到了吧,处理消息用的是 msg.target.dispatchMessage(msg),顾名思义,是用来派发消息的。前面我们说过 Message 有一个字段 when 代表消息预计要处理的时间,target 字段其实代表这个 Message 来自于哪个 Handler,所以这里其实是执行了 handler.dispatchMessage(msg)。
// Handler.java
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
内部执行的是 handleMessage 这是一个接口,当我们使用 Handler 的时候实现一下 handleMessage 就可以处理对应的消息了。
总结
总结一下流程:
子线程:Handler.sendMessage() -> MessageQueue.enqueueMessage()
主线程:ActivityThread.main() -> Looper.loop() -> MessageQueue.next() -> Handler.dispatchMesssage() -> Handler.handleMessage()
而究其原理是利用了线程间内存共享。由于多个线程之间共享内存空间,所以 Handler 可以在线程 A 把消息存放到 MessageQueue,Looper可以在线程 B 把消息取出来,一存一取就实现了线程的切换。
3. Message 在使用的时候是怎么创建的?App 运行过程中创建大量的 Message 不会有什么影响吗?
我们先举一个简单的使用例子:
class MainActivity : AppCompatActivity() {
// 静态内部类,避免内存泄漏
private class MyHandler(activity: MainActivity) : Handler(Looper.getMainLooper()) {
private val activityReference = WeakReference(activity)
override fun handleMessage(msg: Message) {
val activity = activityReference.get()
if (activity != null) {
when (msg.what) {
MESSAGE_UPDATE_TEXT -> {
// 处理消息,更新UI
val text = msg.obj as String
Log.d(TAG, "Message received: $text")
// 假设有一个TextView,更新其文本
// activity.textView.text = text
}
else -> super.handleMessage(msg)
}
}
}
}
// 创建Handler实例
private val handler = MyHandler(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 启动一个子线程
Thread {
// 子线程中的操作
val result = doSomeBackgroundWork()
// 创建消息
val message = handler.obtainMessage(MESSAGE_UPDATE_TEXT, result)
// 发送消息到主线程
handler.sendMessage(message)
}.start()
}
// 模拟后台操作
private fun doSomeBackgroundWork(): String {
// 模拟一些耗时操作
Thread.sleep(2000)
return "Background work completed"
}
}
这个例子中涉及到了前面讲到的内容,而创建消息使用的是 Message.obtain()。
// Message.java
private static Message sPool;
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
可以看到有个重要变量 sPool ,如果 sPool 不存在的话才会新建一个 Message。而 sPool 其实就是一个缓存池,为的就是避免频繁的创建新的 Message 对象。
还记得前面我们说过的 Looper.loop() 方法吗?
// Looper.java
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
// 1. 取出消息
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}
...
try {
// 2. 处理消息
msg.target.dispatchMessage(msg);
...
} finally {
...
}
...
// 3. 回收消息
msg.recycleUnchecked();
return true;
}
在最后,会把消息进行回收 msg.recycleUnchecked():
// Message.java
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 = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
// 属性都置空后会把消息赋值给一个池子
sPool = this;
sPoolSize++;
}
}
}
在回收时会把各个属性都置空的 msg 空壳, 放到 sPool 链表的表头,所以在下次需要创建 Message 的时候,会直接从 sPool 里面取消息。不需要再 new Message() 了。
之所以这样设计,是因为 Android 中每秒都会存在很多 msg,点击、刷新、生命周期、广播等等都是 Message。如果不使用 sPool 复用,那么 msg 被取出后就会被释放掉,所以内存中会频繁的 new 然后 GC 释放内存,造成内存抖动。而内存抖动会导致 OOM、卡顿、内存碎片的增加。
复用机制是使用享元设计模式实现的。这里就不展开讲享元模式了,后面可以单开一节讲设计模式。我们只需要知道享元模式在 Android 源码中很常用,例如 RecyclerView 的复用机制也是用的享元设计模式。
4. Handler 的原理中涉及到休眠,会不会出现 ANR 的问题呢?
什么是 ANR?
ANR 是指应用程序在主线程上执行了耗时操作,导致用户界面无法响应用户交互。简单来说,Android 系统设置了一个阈值,当事件的处理时间超过了这个阈值,那就是 ANR 了。
前面说过 Looper 是在死循环中不断的去从消息队列里面取消息,在取消息时,如果没有到消息该执行的时间也就是当前没有消息要处理,还会调用 nativePollOnce 进行休眠。可能看到休眠两个字,就觉得会产生 ANR,而实际上两者并没有任何关系。
首先在 ActivityThread 启动的时候,Looper 的死循环就随之开始了,可以说我们 APP 的所有代码其实都是在这个死循环中进行的,又怎么会导致 ANR 呢?而 Message 的数量是没有上限的,在每一次刷新屏幕、每一个点击触摸或者说Activity 的生命周期都是通过 Message。
另外 Android 的主线程是基于事件驱动的,当有事件发生时(如用户点击按钮),会生成一个消息并放入消息队列中。Looper.loop() 就会从消息队列中取出这个消息并处理,从而执行相应的逻辑代码。当没有消息处理时,主线程会释放 CPU 资源进入休眠状态,确保线程在没有消息,没有事件需要处理时不会消耗资源,直到下个消息到达。
所以说 Looper 的休眠和 ANR 没有任何关系。ANR 是由于消息处理时间过长或消息队列中积压了大量未处理的消息,而不是因为 Looper.loop() 的休眠。
5. Handler 的内存泄露。
在了解 Handler 的内存泄露之前,需要了解 Java 内存泄漏的一个特性:
Java 中匿名内部类默认持有外部类对象。
这种行为使得匿名内部类可以直接访问外部类的成员变量和方法。
举个例子:
public class OuterClass {
private String outerField = "外部类的字段";
public void doSomething() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(outerField); // 直接访问外部类的私有字段
}
};
runnable.run();
}
}
在匿名内部类中,是可以访问到外部类的私有字段的。
想象一下,在匿名内部类的生命周期比外部类实例更长的情况下,因为匿名内部类持有外部类的引用,所以只要匿名内部类的实例还存在,外部类的实例就不能被垃圾回收器回收,就会造成内存泄漏。
解决方法:
- Handler 使用时用
static修饰,因为在 Java 中静态的匿名内部类不会持有外部类的引用。 - 在外部类销毁时,手动调用
handler.removeCallbacksAndMessages()清理所有未处理的消息和回调。
6. Handler 的消息队列是在哪里被创建的?
看一下 Handler 的构造函数:
public Handler(@Nullable 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;
}
Handler 使用的是 Looper 的 MessageQueue。而 Looper 的 MessageQueue 是在 Looper 的构造函数中被创建的。
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
总结一下,在 ActivityThread 中会通过 prepareMainLooper() 初始化 Looper,Looper 在初始化的过程中创建 MessageQueue,Handler 用的就是这个 MessageQueue。
7. Hanlder 如何退出呢?
退出是通过 Handler 调用 Looper.quit() 方法。
Looper looper = hanlder.getLooper();
if (looper != null) {
looper.quit();
}
// Looper.java
public void quit() {
mQueue.quit(false);
}
public void quitSafely() {
mQueue.quit(true);
}
Looper 调用 MessageQueue 的 quit() 方法。
// MessageQueue.java
void quit(boolean safe) {
...
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
// 删除所有消息
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// 唤醒线程
nativeWake(mPtr);
}
}
Message next() {
...
for (;;) {
...
nativePollOnce(ptr, nextPollTimeoutMillis);
...
if (mQuitting) {
...
// 要退出了,取出的消息设置为null
return null;
}
}
...
}
// Looper.java
public static void loop() {
for (;;) {
Message msg = queue.next();
// 退出循环
if (msg == null) {
return;
}
}
}
当调用 Looper 的 quit 方法时,内部会走到 MessageQueue 的 quit 方法,该方法会清空消息,然后唤醒线程(因为当消息队列没有消息时,线程会挂起)。当线程唤醒后,再次用 Looper 的 loop 方法取消息时,会取出 msg == null。这是在 MessageQueue 的 next() 方法中设置的,即可退出 Loop 循环。
但是,并非所有的线程的 Handler 都可以执行 quit,主线程就是不能被 quit 的。前面提到过 ActivityThread 在 main 中会执行 Looper.prepareMainLooper() 来初始化 Looper。
// Looper.java
public static void prepareMainLooper() {
// quitAllowed = false
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) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// 创建Looper时,会携带quitAllowed参数
sThreadLocal.set(new Looper(quitAllowed));
}
private Looper(boolean quitAllowed) {
// quitAllowed同样会传给MessageQueue
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
// MessageQueue.java
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
...
}
quitAllowed 设置为 false 时,MessageQueue 在执行 quit 时会进行检测,并抛出异常。
之所以这样设计,是因为主线程的 Handler 执行了 Android 中很多重要的事件,例如 Activity、处理 UI 更新和其他重要任务,所以不能随意的把退出主线程的 Looper。
好了,Handler 就说到这里吧,后面想到新的知识点再及时补充~