一、MainThread的Looper创建和准备
Android 的启动过程是: 创建init进程 --> Zygote进程 --> SystemServer进程 --> 各个应用进程 在SystemServer进程启动后(由Zygote进程fork出)在调用run方法时,调用了Looper.prepareMainLooper();,在老版本的则是在ActivityThread.main()中调用的prepareMainLooper
// SystemServer.java package com.android.server; public final class SystemServer { ... public static void main(String[] args) { new SystemServer().run(); } private void run() { ... Looper.prepareMainLooper(); ... startBootstrapService(); startCoreService(); startOtherService(); ... Looper.loop(); ... } ... }于是,主线程在启动后,Activity#onCreate(Bundle) 调用时候就已经准备好Looper了 而工作线程Looper是没有调用prepare()/loop()的,因此需要自己手动调用或者使用HandlerThread/IntentService
遇到一个问Looper存储在哪里的问题
我们来看下Looper.java
// Looper.java ... public static void prepare() { // 工作线程的prepare传入了true,表示允许消息队列退出 prepare(true); } private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { // 在调用prepare的时候如果ThreadLocal中就已经存在Looper了 // 则抛出异常,避免重复prepare和同一个线程出现多个Looper对象 throw new RuntimeException("Only one Looper may be created per thread"); } // 向ThreadLocal 中存入创建的Looper对象 sThreadLocal.set(new Looper(quitAllowed)); } public static void prepareMainLooper() { // 主线程的prepare传入了false,表示主线程的消息队列是不允许对出的!! prepare(false); synchronized (Looper.class) { if (sMainLooper != null) { throw new IllegalStateException("The main Looper has already been prepared."); } // 此处为Looper对象的成员变量sMainLooper初始化 // myLooper()方法实际还是调用ThreadLocal.get() sMainLooper = myLooper(); } }所以我们有答案了,Looper是保存在ThreadLocal这个线程本地存储里的,每一个线程只有一个线程本地存储,所以确保了同一个线程只有一个Looper!
附:ThreadLocal是怎样的数据结构:
// Looper.java // Looper类中有静态常量ThreadLocal对象,通过ThreadLocal.get()获取Looper实例(线程唯一) static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); // ThreadLocal.java // get方法获取了当前线程,通过线程取得线程的ThreadLocalMap引用,然后根据指定泛型对象获取实例 // 简单说就是从Map中获取了ThreadLocal<Looper>的键值对对象Entry,最后返回Entry.value即Looper对象 // 而这个Map是一个哈希数组,key是ThreadLocal<?>对象,就是说只有唯一的一个ThreadLocal<Looper>, // 因为ThreadLocal<Looper>与别的泛型的ThreadLocal对象的hash值不同,因此确保了一个线程只有一个Looper 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(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // Thread.java ThreadLocal.ThreadLocalMap threadLocals = null;总结: 1. 主线程Looper在启动时就已经prepare/loop,子线程需要手动执行或使用HandlerThread/IntentService; 2. Looper被存储在相应Thread的ThreadLocal对象中,这个对象是一个哈希数组,保证了Looper线程唯一;
在loop方法中,实际是一个死循环,即不断的从消息队列中获取消息,并分发给target(Handler)来处; 那么主线程是如何不会被这个死循环卡死的?
简化loop方法如下:
//Looper.java 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 持有的消息队列 final MessageQueue queue = me.mQueue; ... for (;;) { // 进入死循环 不断从消息队列中获取下一个消息 // 官方在这里注释了might block表示获取下一个消息有可能会阻塞 Message msg = queue.next(); // might block if (msg == null) {// 拿到空消息退出循环 这时候loop也退出了 // No message indicates that the message queue is quitting. return; } ... try { // 把非空消息分发给目标Handler msg.target.dispatchMessage(msg); } ... // 对消息进行回收 msg.recycleUnchecked(); } }通过loop方法可以看出端倪,死循环在 queue.next() 方法处阻塞 再看简化的MessageQueue.next方法
// MessageQueue.java Message next() { // MessageQueue持有Native消息队列的指针,如果已经退出了就会返回空消息 final long ptr = mPtr; if (ptr == 0) { return null; } int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { // 再进入一个死循环,调用本地方法去检测描述符有没有新的消息可以poll出来 // 参数nextPollTimeOutMillis表示的是没有检测到新消息时的超时阻塞时间,-1表示一直阻塞 nativePollOnce(ptr, nextPollTimeoutMillis); // 休眠被唤醒后执行同步代码块 synchronized (this) { // 获取系统时间 和 消息队列头部Message final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; // 省略同步屏障有关处理 ... if (msg != null) { if (now < msg.when) { // 如果消息的时间没有到(比如延时消息),重新计算nextPollTimeoutMillis // 下一次循环,nativePollOnce阻塞的时间就是确定的 nextPollTimeoutMillis = (int) Math.min( msg.when - now, Integer.MAX_VALUE); } else { mBlocked = false; if (prevMsg != null) { // 有同步屏障才会走这里 prevMsg.next = msg.next; } else { // 更新消息队列对头 mMessages = msg.next; } // 修改use标记和从消息队列中剥离出msg,最后返回msg msg.next = null; if (DEBUG) Log.v(TAG, "Returning message: " + msg); msg.markInUse(); return msg; } } else { // 取到空消息,重置nextPollTimeoutMillis 继续循环 nextPollTimeoutMillis = -1; } ... } ... }至此,已经找到问题的关键部分了。 Looper.loop()方法的死循环是在MessageQueue.next()获取队列中下一个消息时,阻塞的,并且如果主线程消息队列一直没有新消息时,就会一直阻塞,是被一个nativePollOnce()的本地方法阻塞的。
回到问题,阻塞了为什么没有导致主线程卡死。由于 Linux pipe/epoll机制,主线程会在此时(没有新的消息和事件进入),释放CPU资源,进入休眠状态,等待新的消息或者事件唤醒。
Linux pipe/epoll 机制是一种IO多路复用的机制,基于事件驱动,监测了多个文件描述符。即使没有消息,如果有其他的操作(比如事件)唤醒了主线程,主线程就退出休眠的状态,继续工作,而消息Looper的继续阻塞不会导致主线程卡死。
而在消息机制中,来了新的消息,也会调用一个本地方法唤醒休眠中的主线程
// MessageQueue.java private native static void nativeWake(long ptr);引:
一个线程不仅只有一个Looper,并且也只有一个MessageQueue,这是如何保证的;
如果向队列中发送大量消息,消息又一直在不断被处理,那么为什么不会频繁GC Message导致内存抖动?
带着这两个问题来看源码
第一个问题已经有答案了: 因为Looper由线程私有存储ThreadLocal的哈希数组结构保证线程唯一,MessageQueue又是由Looper实例化的成员变量,那么也是线程唯一的。
在MessageQueue中有个接口IdleHandler,顾名思义是一个空闲时处理任务的处理器。
public static interface IdleHandler { boolean queueIdle(); }这个接口提供了一个返回boolean值的方法。 在MessageQueue中有一个ArrayList<IdleHandler>的集合 此外,还提供了addIdle、removeIdle等方法用于向集合添加和移除IdleHandler。 那么这个IdleHandler有什么作用呢?在何时调用queueIdle方法? 在MessageQueue#next()中有了答案:
Message next() { // 省略了nativePollOnce等逻辑 ... // 在阻塞结束(消息队列中的消息执行完了,而队列中下一个要执行的消息还没有达到可执行的时间)时 if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { pendingIdleHandlerCount = mIdleHandlers.size(); } // 获取列表中IdleHandler个数 if (pendingIdleHandlerCount <= 0) { // 没有IdleHandler直接进入下一次循环 mBlocked = true; continue; } if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } 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 { // 在这里调用了queueIdle方法!!! keep = idler.queueIdle(); } catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); } if (!keep) { synchronized (this) { mIdleHandlers.remove(idler); } } } }也就是当消息队列空闲时,可以执行IdleHandler的一些任务,这样可以提升性能。 比如在onResume方法中,使用主线程的消息队列的IdleHandler做一些准备工作,或者单纯的想获取UI绘制完成的回调,在绘制完成以后立即可以执行一些操作。
在使用Message时,通常建议使用Message.obtain()系列的方法,这是有原因的。 当然也可以通过new Message() 来创建消息,只是更推荐上面的方式,并且new的方式最终也会被做一些响应处理。
由此可以看出消息池最多可以服用的消息对象是50个,这个池是一个单向链表的数据结构;
答案就是,消息对象维护了一个消息池,由于消息本身是一种单向链表的结构,维护对头来复用消息对象,并且在Looper.loop()方法中通过recycleUnchecked()方法来回收消息对象。
Looper获取了消息队列中的消息后,通过Handler#dispathMessage()方法来分发给Handler,就会重发handleMessage方法,于是被重写的handleMessage中的业务逻辑就被执行了。
在日常使用中,经常使用sendMessageDelay/postDelay/sendMesageAtTime等方法,那么延时消息是怎么入队的呢?
// Handler.java public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); }首先,这些方法最终都是调用sendMessageAtTime实现的!
// Handler.java public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) { MessageQueue queue = mQueue; ... return enqueueMessage(queue, msg, uptimeMillis);// }其次,直接调用了enqueueMessage进行入队!这个方法最终调用了MessageQueue#enqueueMessage()方法
那么是怎么实现延时呢? 下面是简化后的MessageQueue#enqueueMessage()方法
// MessageQueue.java boolean enqueueMessage(Message msg, long when) { // 一些可用非空判断 ... 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; } // 标记入队的消息inUse 获取时间 获取消息队列对头 msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake; if (p == null || when == 0 || when < p.when) { // 当消息队列中没有消息或者新入队的消息时间小或为0,需要先执行入队的新消息 // 变更了对头 msg.next = p; mMessages = msg; needWake = mBlocked; } else { // 同步屏障有关省略 ... Message prev; for (;;) { prev = p; p = p.next; // 找到第一个时间比要入队新消息的消息然后放入,或者没有就放队尾 // 实质上是消息队列用when这个时间来排序了,时间小的在队头,从小到大的顺序排列 // 新消息找到正确的入队位置就退出此循环 if (p == null || when < p.when) { break; } // 同步屏障有关省略 ... } // 然后这一步就是真正的入队 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; }答案有了:延时消息也是在触发时就入队了,只是消息队列里面会依据when这个时间标志从小到大屏排序; 那怎样保证延时处理呢?答案就在Looper.loop()方法中,阻塞的过程中有一个nextPollTimeoutMillis参数,如果消息队列中队头就是一个延时消息,那么loop在取消息的时候,会计算一个新的nextPollTimeoutMillis阻塞超时时间,来在delay的时间向Handler交付Message,于是就实现了延时消息!!!
Handler在Activity中使用时常常要注意避免内存泄露,这里泄露的对象就是Activity,常常在使用延时消息时,需要注意如果延时消息还没有处理,Activity就销毁了,那么就会出问题。
MainThread --> ThreadLocal<Looper> --> Looper --> MessageQueue --> Message --> Handler --> Activity
匿名内部类Handler持有Activity的引用,Message.target.dispatchMessage()持有了target(Handler)的引用,就是延时消息持有了Handler的引用,延时消息又enqueue在MessageQueue中,MessageQueue由是由Looper创建的,最终存放在主线程ThreadLocal中了。
只要打破引用链的环节就解决了
让Handler作为静态内部类或者外部类,不持有Activity引用,即可避免Activity泄露,但是此时没有Activity的引用了,可以引入弱引用、软引用;让Handler作为普通内部类使用,但是在Activity生命周期结束时,移除消息队列中的所有回调和消息;(即onDestroy中 调用Handler#removeCallbacksAndMessages一类的remove方法)