Android - 再探Handler
- Handler内存泄漏的原因?怎么规避
- 主线程Handler与子线程Handler的差异
- Looper:消息队列空闲时,死循环为什么不会卡死App?
- MessageQueue:多个Handler并发入队的线程安全、消息屏障
- Looper底层epoll唤醒机制
- ThreadLocal:Looper在线程隔离的原理
- Message:复用池与享元设计模式,正确的创建方式
- 子线程创建Handler标准写法
- HandlerThread原理:如何安全拿到子线程Looper
- 总结
Android开发中,Handler可以说是我们最早接触的异步消息组件。子线程切主线程、UI的更新,几乎每个项目都能见到它的身影。
很多同学对Handler的认知,止步于面试背诵,知道Looper循环、知道匿名内部类会内存泄漏。但是一旦遇到消息屏障、队列空闲处理、epoll阻塞唤醒这类底层问题,就容易理解断层。
本文不会简单罗列API,而是顺着消息发送到完整链路,从Handler、Message、MessageQueue、Looper逐层拆解,从源码入手,读完你不仅能应对面试,遇到相关疑难问题也能从源码角度定位。
本章内容基于对Handler有基础了解的同学,基础薄弱的同学可以先阅读上一章内容,让内容更具连贯性
Handler内存泄漏的原因?怎么规避
classMainActivity:AppCompatActivity(){// 匿名内部类Handlerprivatevalhandler=object:Handler(Looper.getMainLooper()){overridefunhandleMessage(msg:Message){// UI更新逻辑}}overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)// 延时消息,延迟10s执行handler.postDelayed({Log.d("test","延时任务")},10000)}}观察上面这段代码,在打开页面后,立刻关闭Activity。此时的Activity本该被GC回收,但是实际情况却是发生了内存泄漏。
在上述代码中,包含了两处匿名内部类:Handler对象、postDelayed的Runnable,二者都会隐式持有Activity的引用。下面我们将从源码层面拆解这条引用链的形成原因。
// Handler中的数据传递链Handler.java->sendMessage()->sendMessageDelayed()->sendMessageAtTime->enqueueMessage(queue,msg,uptimeMillis);// 将当前Handler对象赋值到Message对象中,确保后续处理的Handler一致// 赋值后,Handler对象就被Message对象持有msg.target=this// 调用MessageQueue对象的enqueueMessage方法用于处理msg对象queue.enqueueMessage(msg,uptimeMillis);MessageQueue.java->enqueueMessage()// 将Handler中的Message放入到MessageQueue队列中msg.next=p;// invariant: p == prev.nextprev.next=msg;// 看到这里,很多人会疑惑:谁持有 MessageQueue?答案是 Looper,下文会展开说明。Looper.java->loop()// loop方法为死循环方法,不断的从MessageQueue队列中取出Message进行处理for(;;){// 取出MessageQueue中的MessageMessagemsg=queue.next();// might block......// 从MessageQueue中获取到Message后,通过dispatchMessage方法分发Msgmsg.target.dispatchMessage(msg);}->Looper()// Looper类的私有构造方法中创建MessageQueue对象并持有MessageQueue对象ActivityThread.java->main()->Looper.prepareMainLooper();->prepare(false);->Looper.sThreadLocal.set(newLooper(quitAllowed))// sThreadLocal作为一个静态的ThreadLocal对象持有Looper对象// 整体的对象引用链sThreadLocal->Looper->MessageQueue->Message->Handler->Activity总结一下,在上述数据传递过程中 整体对象引用链是这样的,在ActivityThread.main()方法中执行Looper.prepareMainLooper();初始化Looper,Looper会把自身存入内部的sThreadLocal静态ThreadLocal对象中,Looper初始化时,同步创建MessageQueue并持有它。回到ActivityThread.main()方法中执行Looper.loop();开始执行loop死循环,不断的取出MessageQueue中的Message对象进行处理,
Message对象中的target字段持有Handler实例,Handler作为一个非静态内部类在Activity中使用,又间接持有了外部Activity实例的引用,形成了引用链,这就是造成内存泄漏的根源。
如何规避,Android中的GC采用的是可达性分析算法,引用链中的sThreadLocal作为GC ROOT,导致引用链中的Activity对象不可被回收,规避方法就是断开引用链,涉及到一个基本的概念:非静态内部类会隐式持有外部类实例对象,静态内部类对象不会持有外部类实例对象。我们这儿就可以改造Handler为静态内部类,同时使用弱引用包装Activity。
修复后的代码
classMainActivity:AppCompatActivity(){// 静态嵌套Handler,不持有外部Activity强引用privateclassMyHandler(activity:MainActivity):Handler(Looper.getMainLooper()){// 弱引用包装ActivityprivatevalweakRef=WeakReference(activity)overridefunhandleMessage(msg:Message){valactivity=ref.get()// 先判断Activity是否存活,不为空再操作UIactivity?.let{// UI更新逻辑}}}privatevalhandler=MyHandler(this)overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)handler.postDelayed({Log.d("test","延时任务")},10000)}overridefunonDestroy(){super.onDestroy()// 页面销毁,移除所有消息,切断引用,根治泄漏handler.removeCallbacksAndMessages(null)}}主线程Handler与子线程Handler的差异
先说差异,再上源码。
主线程启动时,ActivityThread.main()中就会去准备Looper -> Looper.prepareMainLooper(); 所以可以直接在主线程中去使用Handler。
子线程中去实例化Handler对象时,我们需要在创建Handler之前自己去准备Looper -> Looper.prepare();并在其之后执行Looper.loop()方法让MessageQueue循环起来。子线程在Looper使用完毕后,必须手动调用looper.quit()或者looper.quitSafely()终止循环,释放资源,否则子线程会一直阻塞常驻,无法回收。
子线程中不准备Looper直接去new Handler()报错根源,如果不准备Looper,在初始化Handler时就会因为找不到mLooper而直接报错
Handler.java -> Handler() -> mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()"); }需要注意的是: 在主线程中调用的是Looper.prepareMainLooper();子线程中调用的是Looper.prepare();两者的区别在于前者quitAllowed = false;主线程的Looper不允许退出,调用quit()会抛出异常。
if (!mQuitAllowed) { throw new IllegalStateException("Main thread not allowed to quit."); }// ActivityThread中对Looper的初始化 ActivityThread.java -> main() -> Looper.prepareMainLooper(); -> Looper.loop();子线程中使用Handler的正确姿势
Thread{// 第一步:准备Looper,创建MessageQueue并绑定到当前线程Looper.prepare()// 第二步:当前线程Looper就绪,才可以实例化Handlervalhandler=Handler(Looper.myLooper()!!){msg->Log.d("handler","收到消息:${msg.what}")true}handler.sendEmptyMessage(1001)// 第三步:开启消息循环,阻塞等待消息Looper.loop()// 注意:looper退出后,代码才会走到这一行Log.d("handler","looper 已退出,线程结束")}.start()Looper:消息队列空闲时,死循环为什么不会卡死App?
很多同学存在一个误区: Looper.loop()是for(;;)无限循环,不断轮询取Message,会一直占用CPU,造成应用卡死。这个理解是错误的。
先看源码
Looper.java -> loop() // 核心代码片段 for (;;) { Message msg = queue.next(); // might block } MessageQueue.java -> next() -> nativePollOnce(ptr, nextPollTimeoutMillis); // 睡眠 -> enqueueMessage() -> if (needWake) { nativeWake(mPtr); // 唤醒 }关键在于queue.next()方法,源码中的注释为 – might block(可能阻塞)深入该方法后可以看到next()方法中的nativePollOnce()方法,在MessageQueue里没有待处理消息时,会调用nativePollOnce()方法进入阻塞休眠。线程释放CPU,不再占用运算资源。
底层依靠epoll机制监听文件描述符,在有新消息入队时,会写fd唤醒阻塞的线程,继续取出消息执行。所谓的“死循环”并不是忙轮询,是在无消息时进入线程休眠,释放CPU,在有消息时唤醒线程进行消息处理
补充区分:ANR的本质不是Looper死循环。ANR是主线程在处理消息的时候,消息内部执行了耗时操作,导致后续消息长时间得不到处理而造成的,不是loop循环本身造成卡死。loop只是不断取出消息进行执行,本身不会阻塞线程。只有单个Message里面代码耗时,才会ANR。
Looper的循环是阻塞式等待,非忙循环,空闲时线程休眠,不会持续占用CPU,所以不会卡死App。
MessageQueue:多个Handler并发入队的线程安全、消息屏障
MessageQueue底层是单向链表,消息执行时间按照执行时间when从小到大排序,只能从链表头部依次取出。那系统如何实现在不打乱时间排序的前提下,优先处理某些优先级较高的消息呢?
MessageQueue本质是单向链表,并不是并发队列。多个不同的Handler可以同时调用enqueueMessage往队列插入消息,那它如何保证并发安全?
先看源码
MessageQueue.java -> enqueueMessage() boolean enqueueMessage(Message msg, long when) { if (msg.target == null) { throw new IllegalArgumentException("Message must have a target."); } synchronized (this) { if (msg.isInUse()) { throw new IllegalStateException(msg + " This message is already in use."); } 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; } msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake; // 往MessageQueue 消息队列里面添加Handler中的消息 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; } // We can assume mPtr != 0 because mQuitting is false. if (needWake) { nativeWake(mPtr); } } return true; }所有对链表节点的操作都包裹在MessageQueue自身对象锁中。不管多少个Handler、不管在哪个线程调用enqueueMessage,同一时刻只能有一个线程能操作链表。next()取消息方法中,同样将消息取出的操作都包裹在MessageQueue自身对象锁中。
消息屏障(同步屏障)
消息屏障本质是target==null的特殊Message,不会被执行,仅起到标记作用。
依旧源码对比
// 正常的Message Handler.java -> enqueueMessage private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; // target赋值为当前Handler对象 msg.workSourceUid = ThreadLocalWorkSource.getUid(); if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); } // 消息屏障的特殊Message // 没有进行target的赋值 MessageQueue.java -> postSyncBarrier() private int postSyncBarrier(long when) { // Enqueue a new sync barrier token. // We don't need to wake the queue because the purpose of a barrier is to stall it. synchronized (this) { final int token = mNextBarrierToken++; final Message msg = Message.obtain(); msg.markInUse(); msg.when = when; msg.arg1 = token; Message prev = null; Message p = mMessages; if (when != 0) { while (p != null && p.when <= when) { prev = p; p = p.next; } } if (prev != null) { // invariant: p == prev.next msg.next = p; prev.next = msg; } else { msg.next = p; mMessages = msg; } return token; } }在消息取出时,MessageQueue->next(),屏障消息的target为null,通过while循环优先将异步消息取出,优先进行消息发送
// 屏障消息的target是为null的,当发现target为null时 就确定是屏障消息 if (msg != null && msg.target == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. // 通过while循环优先将异步消息取出,优先进行消息的发送 do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); }遇到屏障消息,只处理异步消息(isAsynchronous=true),跳过普通同步消息,在屏障使用完成后,必须手动移除屏障removeSyncBarrier(),否则队列永远不会恢复处理同步消息。
注意:在我们日常用到的普通Handler发送的消息默认都是同步消息,只有设置了msg.isAsynchronous = true的才是异步消息。在Android的底层,用消息屏障做UI渲染,View绘制流程Choreographer依靠消息屏障,优先处理VSYNC信号的异步消息,保证画面刷新的实时性。
补充:postSyncBarrier()和removeSyncBarrier()属于 Android 系统 hide API,应用层无法直接调用。消息屏障是框架内部能力,很典型的场景就是Choreographer的VSYNC信号,保证屏幕刷新新信号优先执行,确保画面的实时刷新。
同时,插入消息屏障时不会唤醒消息队列,屏障的目的就是暂停同步消息的执行。
Looper底层epoll唤醒机制
在前面的章节提到,当MessageQueue没有消息时,MessageQueue.next()会阻塞。这个阻塞能力,就是依赖底层的Linux中的epoll IO多路复用机制。
Handler里的完整epoll工作流程:
- MessageQueue初始化native层时,创建Linux管道pipe:有读fd、写fd
- nativePollOnce:调用epoll_wait,阻塞监听管道读fd。
a. 队列无消息:epoll_wait阻塞,线程休眠,释放CPU
b. 来了新消息:调用nativeWake,往管道写fd写入一个字节数据 - epoll检测到管道读fd有数据就绪,epoll_wait返回,唤醒JAVA层线程,继续执行next()取出消息
- 读完管道里的数据后,清空管道,回到循环继续等待。
注意:epoll是通用IO多路复用,Handler只是拿它做线程休眠/唤醒
epoll是Linux内核提供的IO多路复用技术:可以用单个线程监听多个文件描述符(fd)。当监听的fd没有事件到来时,调用epoll_wait会让当前线程进入休眠,释放CPU;一旦fd有事件就绪,内核唤醒线程继续执行循环。
// 通过epoll_wait()方法阻塞线程 android_os_MessageQueue.cpp -> android_os_MessageQueue_nativePollOnce() -> pollOnce() -> Looper.cpp->int Looper::pollOnce() -> Looper.cpp->int Looper::pollInner() -> // 这就是利用 epoll_wait() 进行阻塞等待,这里的 timeoutMillis是阻塞等待时间,来自Java层的MessageQueue int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis); // 通过往管道中写入fd写入一个数据唤醒线程,继续执行Looper循环 android_os_MessageQueue.cpp -> android_os_MessageQueue_nativeWake() -> wake() -> Looper.cpp->void Looper::wake() -> // 往mWakeEventFd中写入字符1,用以唤醒线程 ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));ThreadLocal:Looper在线程隔离的原理
先回顾ThreadLocal在Handler里的使用场景
// 将Looper set到ThreadLocal中 Looper.java static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } // 将Looper set到ThreadLocal中 sThreadLocal.set(new Looper(quitAllowed)); } // 将Looper从ThreadLocal中get出 Looper.java public static @Nullable Looper myLooper() { return sThreadLocal.get(); }前面我们知道,Looper是绑定到线程的。Looper.myLooper()为什么在不同线程调用,拿到的Looper不一样?怎么做到的线程隔离?
每个Thread对象内部持有ThreadLocalMap,map存储Entry,key为ThreadLocal对象,value存放Looper,不同线程的ThreadLocalMap相互独立,A线程set的数据,B线程读取不到。观察ThreadLocal中的get()方法和set()方法
// get() 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(); } // set() public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } }get()和set()方法中的第一行都是Thread t = Thread.currentThread();拿到当前线程自带的ThreadLocalMap;再以ThreadLocal实例作为key,在该map存取Looper。每个线程的ThreadLocalMap互相独立,A线程存入的数据,B线程读取不到,以此实现线程隔离。
回顾之前章节中提到的主线程创建Handler和子线程创建Handler的区别,调用prepare()时,通过sThreadLocal.set(),将Looper存入当前线程的ThreadLocalMap集合中。调用myLooper(),通过sThreadLocal.get(),读取当前线程绑定到Looper。
如果未执行prepare()方法,在new Handler时会调用Looper.myLooper()方法,获取到的looper==null时则会弹出异常。
这也解释了:子线程没有调用Looper.prepare(),myLooper()拿到null,直接new Handler就抛出异常
Message:复用池与享元设计模式,正确的创建方式
享元设计模式核心:复用已有对象,共享实例,减少内存分配。Message池就是缓存闲置Message对象,重复利用。、
日常写Handler消息时,很多同学习惯性直接new Message()创建实例。但Android源码推荐使用Message.obtain(),背后就是享元设计模式的思想,减少了频繁创建销毁对象带来的内存开销与GC压力。
在Message内部维护了一个静态消息缓存池sPool,本质是一个单向链表,最大缓存数量50个
// Message.java private static Message sPool; private static int sPoolSize = 0; private static final int MAX_POOL_SIZE = 50; // 从池子中获取Message对象 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(); } // 清空Message对象内部值,标记Message为未使用状态重新放回池中 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; // 将Handler对象置空,断开内存泄漏的引用链 callback = null; data = null; synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } }在创建Message对象时,优先从缓存池中取出闲置的Message;缓存中为空时,才new Message()。
消息处理完后,msg.recycle()会调用recycleUnchecked():清空Message全部字段值内容,并且将Message对象标记为未使用状态;如果当前缓存池中的对象未满(50个),则将该Message放回sPool链表中。
在池子满了之后,不再回收Message存入池子中,对象交给GC进行回收。sPoolSync对象锁:多线程并发存取消息池,加锁保证线程安全。
创建Message的正确写法
// 方式1:调用静态obtain方法 val msg = Message.obtain() msg.what = 1001 handler.sendMessage(msg) // 方式2:handler自带obtainMessage // 其内部调用的还是obtain方法 val msg2 = handler.obtainMessage(1001) // 下列代码等同于 handler.sendMessage(msg2) msg2.sendToTarget()Message中的复用缓存池只是缓存优化,不是无限缓存,上限50个;超出50个对象,新new的Message对象会直接交给GC进行处理。
子线程创建Handler标准写法
回顾前面章节,在主线程中App启动时系统自动调用Looper.prepareMainLooper()和Looper.loop();所以在主线程中可以直接 new Handler()
但子线程中默认是不存在Looper的,在new Handler()时会检查是否存在Looper,如果不存在则会抛出异常:"Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()");
想要在子线程中正常使用Handler,有严格的执行顺序:
Looper.prepare() -> new Handler() -> Looper.loop()
public void method() { new Thread(new Runnable() { @Override public void run(){ Looper.prepare(); threadHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { return false; } }); Looper.loop(); // Code } }).start(); }上述代码中展示了在子线程中如何创建一个Handler对象,用于执行Handler发送的Message,但是存在一个弊端,只有通过threadHandler.sendMessage()才能将需要处理的消息发送到当前子线程进行处理。如果存在多个Handler发送的消息需要放到同一个子线程中进行统一的消息处理,该怎么做?
解决方案:定义一个继承Thread的类
public class MyHandler extends Thread{ Looper looper; MyHandler(String name) { super(name); } @Override public void run() { super.run(); Looper.prepare(); // 准备Looper looper = Looper.myLooper(); // 获取到当前线程中的Looper对象 Looper.loop(); } Looper getLooper() {return looper;} // 暴露获取当前线程Looper对象的方法 }但是这样又会存在一个问题,会存在并发的问题,外界线程和当前线程是同步执行,会导致外部线程已经执行完,当前线程还未获取到Looper对象。这就会造成外部获取到的当前Thread的Looper对象未null,解决方案我们接着往下看,系统已经为我们提供了一套完整的解决方案 – HandlerThread.java
HandlerThread原理:如何安全拿到子线程Looper
源码先行:
HandlerThread.java // 线程run方法 public void run() { mTid = Process.myTid(); Looper.prepare(); // 在外部调用HandlerThread子线程start方法时,Synchronized会将主线程挂起等待子线程锁代码块执行完成后再执行主线程 ,确保主线程中调用到的 // Looper不为空 synchronized (this) { // notifyAll(); mLooper = Looper.myLooper(); notifyAll(); // 通知挂起的线程可以执行了,也就是getLooper方法中的Looper可以返回了 //code() } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); //不会退出 mTid = -1; } // 提供给外部的获取Looper的方法 public Looper getLooper() { if (!isAlive()) { return null; } // 在主线程中,如果子线程没有获取到CPU的执行时间片,主线程优先执行,就会在主线程中先执行到getLooper()方法上,这儿通过锁并挂起当前线程并释放锁,等待子线程获取到CPU // 执行时间片后在run方法中唤醒当前挂起的线程 再获取到对应的looper // If the thread has been started, wait until the looper has been created. synchronized (this) { // 为什么使用的是while循环 而不是用if循环 // 这是一种设计思想,原因: 如果在其他地方,调用了当前线程的notifyAll() 这就会导致当前挂起的线程恢复执行,返回当前的looper,但是可能当前的Looper还是null // 如果采用if 只判断一次,可能会造成返回的looper还是null while (isAlive() && mLooper == null) {//?? if try { wait();// 将线程挂起,并释放当前的锁给其他地方使用 //code() } catch (InterruptedException e) { } } } return mLooper; }逻辑执行流程:
1. 主线程调用handlerThread.start(),开启子线程
2. 主线程立刻调用getLooper(),此时子线程run还没执行完prepare()方法,mLooper还是null,主线程wait()阻塞。
3. 子线程run执行Looper.prepare(),拿到Looper赋值给mLooper,执行notifyAll()唤醒主线程
4. 主线程被唤醒后,退出while循环,返回已经初始化好的Looper,外部就可以正常创建Handler对象了。
总结
我们从Handler内存泄漏、主线程与子线程Handler差异入手,逐层拆解了解整套消息通信机制。
一条完整的消息流转链路:Handler.sendMessage() -> Message入队MessageQueue -> Looper不断调用queue.next()取出消息 -> 底层epoll机制实现空闲阻塞与消息唤醒 -> 最终交给Handler.handleMessage()处理
1. ThreadLocal:Looper和线程一一绑定,保证一个线程仅存在一个唯一的Looper
2. Message复用池(享元设计模式):复用Message对象,减少频繁创建销毁对象带来的GC开销
3. 消息屏障:框架内部机制,优先调度处理异步消息,支撑着Choreographer的UI绘制。
4. HandlerThread:系统提供的封装好的子线程Looper样板代码
Handler相关至此结束了,下一篇将开启Binder IPC通信机制的学习啦!