「Handler」,老生常谈,网上关于它的文章可谓是“泛滥成灾”,不过实际开发中用得不多。毕竟,现在写异步,「RxAndroid链式调用」就是「Kotlin协程同步方式写异步代码」,香?不过,面试官还是喜欢「章口就莱」一句:什么叫做国际巨星啊 不好意思读错台词…
当然,应对方法也很简单,找一篇《…Handler详解》之类的文章,背熟即可~
不过,对于我这种好 寻花问柳 寻根问底的人来说,自己过一遍源码心理才踏实,
而且,我发现「带着问题」看源码,思考理解本质,印象会更深,收获也更多,遂有此文。
罗列一波本文提及的问题,如果有答不出的可按需阅读本文,谢谢~
- 1、Handler问题三连:是什么?有什么用?为什么要用Handler,不用行不行?
- 2、真的只能在主(UI)线程中更新UI吗?
- 3、真的不能在主(UI)线程中执行网络操作吗?
- 4、Handler怎么用?
- 5、为什么建议使用Message.obtain()来创建Message实例?
- 6、为什么子线程中不可以直接new Handler()而主线程中可以?
- 7、主线程给子线程的Handler发送消息怎么写?
- 8、HandlerThread实现的核心原理?
- 9、当你用Handler发送一个Message,发生了什么?
- 10、Looper是怎么拣队列里的消息的?
- 11、分发给Handler的消息是怎么处理的?
- 12、Looper在主线程中死循环,为啥不会ANR?
- 13、Handler泄露的原因及正确写法
答:Android定义的一套「子线程与主线程间通讯」的「消息传递机制」。
答:把子线程中的 UI更新信息,传递 给主线程(UI线程),以此完成UI更新操作。
答:不行,因为android在设计之初就封装了一套消息创建、传递、处理。如果不遵循就不能更新UI信息,就会报出异常(所谓的异步消息处理)
在Android中,为了提高系统运行效率,没有采用「线程锁」,带来了:
多个线程并发更新UI时的线程安全问题
为了安全保证UI操作是线程安全的,规定
只能在主线程(UI线程)中完成UI更新
但,真的只能在UI线程中更新UI吗?
上面这段代码 直接在子线程中更新了UI,却没有报错:
这是要打脸吗?但如果在子线程中加句线程休眠模拟耗时操作的话:
程序就崩溃了,报错如下:
翻译一下异常信息:只有创建这个view的线程才能操作这个view。限于篇幅,这里就不去跟源码了,直接说原因:
ViewRootImp 在 onCreate() 时还没创建;
在 onResume()时,即ActivityThread的handleResumeActivity() 执行后才创建,
调用 requestLayout(),走到 checkThread() 时就报错了。
可以打个日志简单的验证下:
加上休眠:
行吧,以后去面试别人问「子线程是不是一定不可以更新UI」别傻乎乎的说是了。
说到「只能在主线程中更新UI」我又想到另一个问题「不能在主线程中进行网络操作」
上述代码运行直接闪退,日志如下:
NetworkOnMainThreadException:网络请求在主线程进行异常。
em… 真的不能在主线程中做网络操作吗?
在 onCreate() 的 setContentView() 后插入下面两句代码:
运行下看看:
这…又打脸?先说下 StrictMode(严苟模式)
Android 2.3 引入,用于检测两大问题:ThreadPolicy(线程策略) 和 VmPolicy(VM策略)
相关方法如下:
把严苟模式的网络检测关了,就可以 在主线程中执行网络操作了,不过一般是不建议这样做的:
在主线程中进行耗时操作,可能会导致程序无响应,即 ANR (Application Not Responding)。
至于常见的ANR时间,可以在对应的源码中找到:
// ActiveServices.java → Service服务 static final int SERVICE_TIMEOUT = 20*1000; // 前台 static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10; // 后台 // ActivityManagerService.java → Broadcast广播、InputDispatching、ContentProvider static final int BROADCAST_FG_TIMEOUT = 10*1000; // 前台 static final int BROADCAST_BG_TIMEOUT = 60*1000; // 后台 static final int KEY_DISPATCHING_TIMEOUT = 5*1000; // 关键调度 static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10*1000; // 内容提供者 复制代码
时间统计区间:
- 起点:System_Server 进程调用 startProcessLocked 后调用 AMS.attachApplicationLocked()
- 终点:Provider 进程 installProvider及publishContentProviders 调用到 AMS.publishContentProviders()
- 超过这个时间,系统就会杀掉 Provider 进程。
代码示例如下:
黄色部分会有如下警告
Handler不是静态类可能引起「内存泄露」,原因以及正确写法等下再讲。
另外,建议调用 Message.obtain() 函数来获取一个Message实例,为啥?点进源码:
从源码可以看到obtain()的逻辑:
- 1、判断Message池是否为空;
- 2、不为空,取出一个Message对象,池容量-1,返回;
- 3、否则,新建一个Message对象,返回;
这样可以「避免重复创建多个实例对象」节约内存,还有,Message池其实是一个「单链表结构」,定位到下述代码可以看到:池的容量为50
然后问题来了,Message信息什么时候加到池中?
当Message 被Looper分发完后,会调用 recycleUnchecked()函数,回收没有在使用的message对象。
如果你懂点数据结构的话,可以看出这是「单链表的头插法」
代码示例如下:
跟下post():
实际上调用了 sendMessageDelayed() 发送消息,只不过延迟秒数为0,
那Runnable是怎么变成Message的呢?跟下getPostMessage()
噢,获取一个新的Message示例后,把Runnable变量的值赋值给callback属性
activity.runOnUiThread()
view.post() 与 view.postDelay()
终于来到稍微有点技术含量的缓解,在观摩源码了解原理前,先说下几个涉及到的类。
在我们使用Handler前,Android系统已为我们做了一系列的工作,其中就包括了
创建「Looper」和「MessageQueue」对象
上图中有写:ActivityThread的main函数是APP进程的入口,定位到 ActivityThread → main函数
// ActivityThread.main() Looper.prepareMainLooper(); // 创建Looper和MessageQueue对象,用于处理主线程消息 ActivityThread thread = new ActivityThread(); // 实例化ActivityThread对象 thread.attach(false, startSeq); // 与主线程进行绑定 Looper.loop(); // 消息循环运行,死循环,正常不会执行后续代码 throw new RuntimeException("Main thread loop unexpectedly exited"); 复制代码
定位到:Looper → prepareMainLooper函数
prepare(false) // 设置Looper不可关闭 复制代码
定位到:Looper → prepare函数
sThreadLocal.set(new Looper(quitAllowed)); // 创建Looper 复制代码
定位到:Looper → Looper构造函数
mQueue = new MessageQueue(quitAllowed); // Looper与MessageQueue绑定 mThread = Thread.currentThread(); // Looper与线程绑定 复制代码
另外这里的 quitAllowed 变量,直译「退出允许」,具体作用是?跟下 MessageQueue:
em…用来 防止开发者手动终止消息队列,停止Looper循环。
前戏过后,创建了Looper与MessageQueue对象,接着调用Looper.loop()开启轮询。
定位到:Looper → loop函数
// Looper.loop() final Looper me = myLooper(); // 获得当前线程的Looper实例 final MessageQueue queue = me.mQueue; // 获取消息队列 for (;;) { // 死循环 Message msg = queue.next(); // 取出队列中的消息 msg.target.dispatchMessage(msg); // 将消息分发给Handler } 复制代码
接着有几个问题,先是这个 myLooper() 函数:
这里的 ThreadLocal → 线程局部变量 → JDK提供的用于解决线程安全的工具类。
作用:为每个线程提供一个独立的变量副本 → 以解决并发访问的冲突问题。
本质:以当前线程的Id为key,存储该线程的值,即每个线程只能获取到自己set的数据。
所以:主线程和子线程的Looper对象实例相互隔离的!!!
即:主线程和子线程Looper不是同一个!!!
知道这个以后,有个问题就解惑了:
「为什么子线程中不能直接 new Handler(),而主线程可以?」
答:主线程与子线程不共享同一个Looper实例,主线程的Looper在启动时就通过
prepareMainLooper() 完成了初始化,而子线程还需要调用 Looper.prepare()
和 Looper.loop()开启轮询,否则会报错,不信,可以试试:
直接就奔溃了~
加上试试?
可以,程序正常运行,没有报错。
对了,既然说Handler用于子线程和主线程通信,试试在主线程中给子线程的Handler发送信息,修改一波代码:
运行,直接报错:
原因:多线程并发的问题,当主线程执行到sendEnptyMessage时,子线程的Handler还没有创建。
一个简单的解决方法是:主线程延时给子线程发消息,修改后的代码示例如下:
运行结果如下:
可以,不过其实Android已经给我们封装好了一个轻量级的异步类「HandlerThread」
HandlerThread = 继承Thread + 封装Looper
使用方法很简单,改造下我们上面的代码:
用法挺简单的,源码其实也很简单,跟一跟:
剩下一个quit()和quitSafely()停止线程,就不用说了,所以HandlerThread的核心原理就是:
- 继承Thread,getLooper()加锁死循环wait()堵塞;
- run()加锁等待Looper对象创建成功,notifyAll()唤醒;
- 唤醒后,getLooper返回由run()中生成的Looper对象;
是吧,HandlerThread的实现原理竟简单如斯,另外,顺带提个醒!!!
Java中所有类的父类是 Object 类,里面提供了wait、notify、notifyAll三个方法;
Kotlin 中所有类的父类是 Any 类,里面可没有上述三个方法!!!
所以你不能在kotlin类中直接调用,但你可以创建一个java.lang.Object的实例作为lock,
去调用相关的方法。
代码示例如下:
private val lock = java.lang.Object() fun produce() = synchronized(lock) { while(items>=maxItems) { lock.wait() } Thread.sleep(rand.nextInt(100).toLong()) items++ println("Produced, count is$items:${Thread.currentThread()}") lock.notifyAll() } fun consume() = synchronized(lock) { while(items<=0) { lock.wait() } Thread.sleep(rand.nextInt(100).toLong()) items-- println("Consumed, count is$items:${Thread.currentThread()}") lock.notifyAll() } 复制代码
扯得有点远了,拉回来,刚讲到 ActivityThread 在 main函数中调用 Looper.prepareMainLooper
完成主线程 Looper初始化,然后调用 Looper.loop() 开启消息循环 等待接收消息。
嗯,接着说下 发送消息,上面也说了,Handler可以通过sendMessage()和 post() 发送消息,
上面也说了,源码中,这两个最后调用的其实都是 sendMessageDelayed()完成的:
第二个参数:当前系统时间+延时时间,这个会影响「调度顺序」,跟 sendMessageAtTime()
获取当前线程Looper中的MessageQueue队列,判空,空打印异常,否则返回 enqueueMessage(),跟:
这里的 mAsynchronous 是 异步消息的标志,如果Handler构造方法不传入这个参数,默认false:
这里涉及到了一个「同步屏障」的东西,等等再讲,跟:MessageQueue -> enqueueMessage
如果你了解数据结构中的单链表的话,这些都很简单。
不了解的可以移步至【面试】数据结构与算法(二) 学习一波~
MessageQueue里有Message了,接着就该由Looper来分拣了,定位到:Looper → loop函数
// Looper.loop() final Looper me = myLooper(); // 获得当前线程的Looper实例 final MessageQueue queue = me.mQueue; // 获取消息队列 for (;;) { // 死循环 Message msg = queue.next(); // 取出队列中的消息 msg.target.dispatchMessage(msg); // 将消息分发给Handler } 复制代码
queue.next() 从队列拿出消息,定位到:MessageQueue -> next函数:
这里的关键其实就是:nextPollTimeoutMillis,决定了堵塞与否,以及堵塞的时间,三种情况:
等于0时,不堵塞,立即返回,Looper第一次处理消息,有一个消息处理完 ;
大于0时,最长堵塞等待时间,期间有新消息进来,可能会了立即返回(立即执行);
等于-1时,无消息时,会一直堵塞;
Tips:此处用到了Linux的pipe/epoll机制:没有消息时阻塞线程并进入休眠释放cpu资源,有消息时唤醒线程;
通过MessageQueue的queue.next()拣出消息后,调用msg.target.dispatchMessage(msg)
把消息分发给对应的Handler,跟到:Handler -> dispatchMessage
到此关于Handler底层原理也说得七七八八了!
答:上面说了,Looper通过queue.next()获取消息队列消息,当队列为空,会堵塞,
此时主线程也堵塞在这里,好处是:main函数无法退出,APP不会一启动就结束!
你可能会问:主线程都堵住了,怎么响应用户操作和回调Activity声明周期相关的方法?
答:application启动时,可不止一个main线程,还有其他两个Binder线程:ApplicationThread 和 ActivityManagerProxy,用来和系统进程进行通信操作,接收系统进程发送的通知。
- 当系统受到因用户操作产生的通知时,会通过 Binder 方式跨进程通知 ApplicationThread;
- 它通过Handler机制,往 ActivityThread 的 MessageQueue 中插入消息,唤醒了主线程;
- queue.next() 能拿到消息了,然后 dispatchMessage 完成事件分发;
PS:ActivityThread 中的内部类H中有具体实现
死循环不会ANR,但是 dispatchMessage 中又可能会ANR哦!如果你在此执行一些耗时操作,
导致这个消息一直没处理完,后面又接收到了很多消息,堆积太多,从而引起ANR异常!!!
上面说了,如果直接在Activity中初始化一个Handler对象,会报如下错误:
原因是:
在Java中,非静态内部类会持有一个外部类的隐式引用,可能会造成外部类无法被GC;
比如这里的Handler,就是非静态内部类,它会持有Activity的引用从而导致Activity无法正常释放。
而单单使用静态内部类,Handler就不能调用Activity里的非静态方法了,所以加上「弱引用」持有外部Activity。
代码示例如下:
private static class MyHandler extends Handler { //创建一个弱引用持有外部类的对象 private final WeakReference<MainActivity> content; private MyHandler(MainActivity content) { this.content = new WeakReference<MainActivity>(content); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); MainActivity activity= content.get(); if (activity != null) { switch (msg.what) { case 0: { activity.notifyUI(); } } } } } 复制代码
转换成Kotlin:(Tips:Kotlin 中的内部类,默认是静态内部类,使用inner修饰才为非静态~)
private class MyHandler(content: MainActivity) : Handler() { //创建一个弱引用持有外部类的对象 private val content: WeakReference<MainActivity> = WeakReference(content) override fun handleMessage(msg: Message) { super.handleMessage(msg) val activity = content.get() if (activity != null) { when (msg.what) { -> { activity.notifyUI() } } } } } 复制代码
总算把Handler过了一遍,有点凌乱,同步栅栏也还么写,等找到工作,再整理补全吧,谢谢~
参考文献: