Handler 源码分析面试的疑难杂症

引言

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 对象引用了HandlerMessage-》Handler

最后消息都要放到消息队列去 MessageQueueMessageQueue -》Message

MessageQueue

继续分析源码 我们看看 MessageQueueenqueueMessage 方法 源码有点长 慢慢分析下

    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

引入第二个问题 Message正确创建姿势?

对于新手而言 这个问题应该很懵吧 创建一个对象不是直接 new 一个就好了嘛 程序员的对象不都是 new 出来的 那既然能问出这个问题 那他肯定是有意义的 正确答案是 调用 Messageobtain 静态方法 他会返回一个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 链表 上面的代码很好理解

取出头节点 清除 nextflags 标记 然后直接返回了 如果这个缓存池是空的话直接 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 直接给置 nullGC 回收而是把Message 中的所有 标记内容 全部置空 然后进行回收 已备下次使用。这里对 sPool 是如果来的就明白了。这种模式称为 享元模式

那你可能又抛出另一个问题了 为什么要这样的 不是显得更为麻烦?

AndroidHandler 算是重中之重了 这也是面试官为什么那么喜欢问的原因吧。没有了Handler 整个 App 都跑不起来 我们 四大组件 的生命周期回调 等等的细节 都是有 Handler 在背后默默进行工作。也就是使用非常的频繁。而 Message 这种模式就是避免了大量一直创建 Message 对象。这也是享元模式的意义。如果频繁创建对象,使用完 GC 回收。这样会产生内存抖动,严重点会发生 ANR

明白了 Message 我们就开始着手看看 ifelse

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对象) 调用 HandlerdispatchMessage 最后回收 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呢 其实在打开 AppActivityThread 就默默帮我们初始化了 精简代码 如果

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设计的内容较广 希望这篇文章能给你带来帮助

最后我想说 如果真的想学习源码 一定要亲自自己去看一遍源码。这也是我第一次看源码,我给出一点个人建议。

  • 看源码前 可以看几篇好的源码解释博客 多看几篇加深下印象(不同的作者不同的思路)
  • 看完博客后 自己学者独立去看源码 可能里面还有很多神奇的代码 排除非核心 一些不懂的 留个心眼 等大体看明白了 也许你就会有答案了
  • 看完源码后 做一个总结 学会反思 学习源码里面的精华 为什么要这么写 等等
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值