引言
Hanlder 为什么会内存泄漏?为什么Looper死循环不会卡死应用?为什么Handler能用来切换线程?Handler、Looper、MessageQueue、ThreadLocal之间什么关系?接下来看源码学习学习。
Handler 应该是 老生常谈 为什么那么喜欢 Handler 呢?
笔者认为 Handler 是驱动 整个 APP
的关键类 没有它 Activity、Service 等组件都无法正常回调生命周期,但他还不止这些呢。还有一个是Android 只允许主线程去刷新线程 其他非 UI
线程 无法刷新 UI
那么怎么切换线程呢?还是Handler去做的 所以它的任务量很重啊。
在讲Handler时 我们看一幅图
Looper 是动力源泉
Message 是消息
MessageQueue 是包裹信息的容器
流程:准备好 Looper对象 然后再创建一个 Handler 对象 最后 启动Looper给足动力 ,在线程中通过 创建的Handler#postXxx或 Handler#sendXxx 方法 发送消息 ,消息会被放再MessageQueue容器中 然后通过Looper提供的动力 消息会一个个传达出去 需要注意的是 里面的消息是有时序的,因为有一些包裹是有特殊要求的 需要推迟送达。
正文开始
Hanlder
首先创建一个Handler对象
private val mHandler = object :Handler(mainLooper){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
}
}
通过调用 mHandler
的各种发送消息方法 如 postDelayed
sendMessage
等一系列发送消息的方法最终都会调用 sendMessageAtTime
方法 第一个参数是 message
第二个参数需要注意是 SystemClock.uptimeMillis() + delayMillis
是当前时间 加上 所需要延迟的时间 当然不需要等待就为0 这个时间需要记住 待会都会用到。
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
return sendMessageDelayed(getPostMessage(r), delayMillis);
}
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
在 sendMessageAtTime
中 他先获取消息队列 判断是否为空 空的话直接抛出异常 否者将调用enqueueMessage
源码如下
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
最终 会调用 enqueueMessage
进行排队 源码如下
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this; // *注意这个target 下面问题的关键
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
到这里停一下 引入一个问题 为什么
Handler
会引发内存泄漏 ?
我们在使用Handler时 会创建一个匿名内部类 匿名内部类有一个特点会默认持有一个外部类的对象
class Test extends AppCompatActivity {
Handler handler = new Handler(getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
x = 3;
Test.this.x = 2; // == x=3;
}
};
private int x = 1;
}
这里 Handler
持有 Activity
而在 enqueueMessage
方法中 Message
中的 target
持有了 Handler
msg.target = this;
而 Message
对象又在 MessageQueue
的队列中
至此 MessageQueue
-> Message
-> Handler
-> Activity
如果这个时候有延迟消息 还没处理时 Activity
销毁 而此时 Activity
又被引用 不能被 GC
销毁 这里就照成了 内存泄漏 。
是不是引用来引用去有点乱 总结下 :
Handler 在创建匿名内部类时 默认会引用 外部类对象 也就是 Activity 【
Handler
-》Activity
】当我们向
Handler
发送消息时 如sendMessage
一系列方法等 最终会调用 Handler 的enqueueMessage
方法 该方法将Message
对象引用了Handler
【Message
-》Handler
】最后消息都要放到消息队列去
MessageQueue
【MessageQueue
-》Message
】
MessageQueue
继续分析源码 我们看看 MessageQueue
的 enqueueMessage
方法 源码有点长 慢慢分析下
boolean enqueueMessage(Message msg, long when) {
//。。。忽略代码
synchronized (this) {
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// 新头,如果被阻塞,唤醒事件队列。
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 插入到队列的中间。通常我们不需要醒来
// 在事件队列中,除非队列头部有障碍
// 消息是队列中最早的异步消息。
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;
}
在看上面这段代码时 你会发现 Message是好像是一个链表 伪代码如下
boolean enqueueMessage(Message msg, long when) {
Message p = mMessages;
msg.next = p;
}
Message
而在 Message
中 他的 next
变量就是 Message
类型。
引入第二个问题 Message正确创建姿势?
对于新手而言 这个问题应该很懵吧 创建一个对象不是直接 new
一个就好了嘛 程序员的对象不都是 new
出来的 那既然能问出这个问题 那他肯定是有意义的 正确答案是 调用 Message
的 obtain
静态方法 他会返回一个Message
对象。为什么不直接 new
呢?我们来看看 obtain
的源码
private static Message sPool;
public static Message obtain() {
synchronized (sPoolSync) {
f (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
在知道了 Message
是链接结构,sPool
而这个变量就是缓存使用过的 Message
链表 上面的代码很好理解
取出头节点 清除 next
和 flags
标记 然后直接返回了 如果这个缓存池是空的话直接 new
出来一个 Message
对象 这里再提出一个疑问 sPool
这个缓存池的对象是怎么来的呢?
Looper
在消费完 Message
对象时 会调用 recycleUnchecked
方法
void recycleUnchecked() {
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++;
}
}
}
可以看到 他并不是直接把使用过的 Message
直接给置 null
让 GC
回收而是把Message 中的所有 标记
和 内容
全部置空 然后进行回收 已备下次使用。这里对 sPool
是如果来的就明白了。这种模式称为 享元模式。
那你可能又抛出另一个问题了 为什么要这样的 不是显得更为麻烦?
在 Android
中 Handler
算是重中之重了 这也是面试官为什么那么喜欢问的原因吧。没有了Handler
整个 App
都跑不起来 我们 四大组件 的生命周期回调 等等的细节 都是有 Handler
在背后默默进行工作。也就是使用非常的频繁。而 Message
这种模式就是避免了大量一直创建 Message
对象。这也是享元模式的意义。如果频繁创建对象,使用完 GC
回收。这样会产生内存抖动,严重点会发生 ANR
。
明白了 Message
我们就开始着手看看 if
和 else
了
Message p = mMessages;
//如果 P等于空 或者 时间=0 或者 当前消息的时间 < 链表头的消息时间
//那么把当前消息置于头部 也就是 链表起点
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
} else {
Message prev;
// 这里开启了一个死循环 只有当 P=null 或者 when<P.when 时才会跳出循环
// 这段代码 主要将传入的 msg 进行时间上的排序 按照 when 时间顺序依次连接
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
Looper
到这里 MessageQueue
对象也搞明白了 我们来看看 Looper
等下 我们转个方式看源码吧 这我们带着问题去看源码吧
子线程如何使用
Handler
?为什么主线程可以直接使用Handler
?一个线程有几个Looper
?一个Looper
有几个MessageQueue
?
首先Looper的构造函数是私有的 也就是我们不能直接实例化它 那么怎么实例化它呢?
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
用过的小伙伴就会说prepare
对就是这个 prepare
方法 我们看看源码
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));
}
分析下 上面的方法 顺便解答下 的其中一个问题 sThreadLocal.get()
从源码上看 第一个if 就判断 looper 是否为空了 如果不为空将抛出异常Only one Looper may be created per thread
这里就保证只能初始化一次。如果不为空 就 new
一个 Looper
放在 sThreadLocal
这个是 ThreadLocal
是什么呢?防止有小伙伴不懂 介绍下
ThreadLocal
用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal
直接获取到,避免了传参,类似于全局变量的概念。
在 Looper
里 用ThreadLocal
存放 自己的实例 Looper
这样每个线程的 Looper
对象 互不干扰 独立运行 也保证了一个线程只有一个 Looper
对象 也更方便获取 Looper
不需要传递参数。这也是 ThreadLocal
的巧妙之处。
Looper
初始化完成了 接下来要让滚轮转起来了 loop
方法 源码如下。。 太长了 精简下
public static void loop() {
//。。忽略代码段
for (;;) {
Message msg = queue.next();//获取队列消息 获取不到将会阻塞
if (msg == null) {
// 没有消息表明消息队列退出。
return;
}
//回调我们的Handler 让他去消费消息这个方法最终 会回到 handleMessage 方法
msg.target.dispatchMessage(msg);
//回收msg对象
msg.recycleUnchecked();
}
//。。忽略代码段
}
上面的代码精简下 核心就是 一个死循环 然后通过 MessageQueue
队列 获取 Message
对象 如果获取不到 将阻塞在这里,如果获取到了 将直接消费它 (target
我们前面解释了 它就是一个 Handler
对象) 调用 Handler
的 dispatchMessage
最后回收 msg
对象。
dispatchMessage
方法 如果回调 handleMessage
方法最终跑到主线程运行的呢 看下源码
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
我们在创建 Handler时 使用匿名内部类 实现的 handleMessage
的重写,在 dispatchMessage
中直接调用
回到 loop 方法继续分析 queue.next()
这个方法是调用 MessageQueue
的next方法 太长了 精简下
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) {
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 {
// 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 {
nextPollTimeoutMillis = -1;
}
//。。
nativePollOnce
值得关注是这个方法 它是通过native层 去阻塞当前 此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的 epoll
机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 **主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。**这里也解释了 为什么 Looper
的 死循环不会卡死线程
到这里 我们应该明白子线程怎么使用Handler了吧
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
那么主线程为什么 可以直接使用Handler 都不需要初始化Looper呢 其实在打开 App
时 ActivityThread
就默默帮我们初始化了 精简代码 如果
ActivityThread.class
public static void main(String[] args) {
//。。。
Looper.prepareMainLooper();
//。。。
Looper.loop();
//。。。
}
到这里 你可以再回到我们一开始的流程图看看 是不是略懂不少 那么上面几个问题 心中是否有了答案?
继续引用问题
怎么再线程里使用主线程的Handler 为什么它可以使用主线程的Handler?
我们知道再子线程使用主线程的Handler 只需要再创建Handler时 传入Main的Looper 源码如下
thread {
val x = Handler(Looper.getMainLooper()).post {
Toast.makeText(this, "${Thread.currentThread().name}", Toast.LENGTH_SHORT).show()
}
}
那这样写为什么就可以跑到主线程呢 看看Handler的构造函数吧 看看它做了什么 源码如下
public Handler(@NonNull Looper looper) {
this(looper, null, false);
}
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
这里你心中是否有答案了呢 传入主线程的Looper 而Looper绑定着MessageQueue 这里就获取了到了主线程的Queue 并且把当前的Queue 绑定到Handler的Queue里 再Handler发送消息时 获取当前的Message 然后添加Queue里 到这里 我们就把消息存放到了主线程的MessageQueue队列里 而主线程的Looper 在死循环中获取消息 到这里消息就成功在主线程中跑起来了
那么这里又可以引出一个问题
为什么主线程下Looper不会卡死应用?或发送ANR
在 java 中 我们知道 一个Main函数跑完 程序就结束运行了 Android也是如此 正如上面的代码一样 就因为Looper 会一直在死循环 所以也保证了 当前应用会一直运行着
public static void main(String[] args) {
//。。。
Looper.prepareMainLooper();
//。。。
Looper.loop();
//。。。
}
而在looper 中会通过 MessageQueue 中 next 函数 获取 Message next函数又是带阻塞的 为什么不会发送ANR 呢 其实对 ANR 有点误解 ANR发送的原因是 消息无法及时响应 比如在主线程休眠 10s 而next方法我们前面介绍了 他是linux中的 epoll
机制 所以和ANR是两个完全不同的概念 自然无法比较。
最后一个问题
下面代码中 是否能成功打印出Log?
thread {
Looper.prepare()
val x = Handler().post {
Toast.makeText(this, "${Thread.currentThread().name}", Toast.LENGTH_SHORT).show()
}
Looper.loop()
Log.d("Hello","World")
}
怎么样 乍一看 没上面问题啊
答案是否定的 为什么呢 因为Looper处于死循环 也就是循环没有结束 他是不会执行下面的代码块。那怎么停止loop死循环呢 在loop源码中 唯一能停止死循环的办法就是 获取的 msg 为空 就跳出死循环 源码如下
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// 没有消息表明消息队列退出。
return;
}
}
那怎么停止死循环呢?Looper也给我们准备好了 调用 quit 方法 源码如下
public void quit() {
mQueue.quit(false);
}
调用了MessageQueue的quit方法继续看看源码
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);
}
}
private void removeAllMessagesLocked() {
Message p = mMessages;
while (p != null) {
Message n = p.next;
p.recycleUnchecked();
p = n;
}
mMessages = null;
}
是不是明白了怎么退出的 不知道你在这里有没有想过 loop 结束了循环 那么就可以往下执行了 那如果 主线程的loop 结束循环 是不是就意味着 APP 整个就结束了 我们试着调用下
Looper.getMainLooper().quit()
会发现直接抛出异常了 “Main thread not allowed to quit.” 我们回头上面MessageQueue 的 quit 方法 会发现 他阻止了这样的操作
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
总结
源码分析结束了 到这里我们大致理清楚了Handler的原理 会解答了面试经常问到了几个问题 你可以带着问题 在自己去看一面源码 看看是否能从中找出答案 加深一下印象 Handler设计的内容较广 希望这篇文章能给你带来帮助
最后我想说 如果真的想学习源码 一定要亲自自己去看一遍源码。这也是我第一次看源码,我给出一点个人建议。
- 看源码前 可以看几篇好的源码解释博客 多看几篇加深下印象(不同的作者不同的思路)
- 看完博客后 自己学者独立去看源码 可能里面还有很多神奇的代码 排除非核心 一些不懂的 留个心眼 等大体看明白了 也许你就会有答案了
- 看完源码后 做一个总结 学会反思 学习源码里面的精华 为什么要这么写 等等