「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泄露的原因及正确写法
0x1、Handler问题三连
1.Handler是什么
答:Android定义的一套「子线程与主线程间通讯」的「消息传递机制」。
2.Handler有什么用
答:把子线程中的 UI更新信息,传递 给主线程(UI线程),以此完成UI更新操作。
3.为什么要用Handler,不用行不行
答:不行,因为android在设计之初就封装了一套消息创建、传递、处理。如果不遵循就不能更新UI信息,就会报出异常(所谓的异步消息处理)
在Android中,为了提高系统运行效率,没有采用「线程锁」,带来了:
多个线程并发更新UI时的线程安全问题
为了安全保证UI操作是线程安全的,规定
只能在主线程(UI线程)中完成UI更新
但,真的只能在UI线程中更新UI吗?
上面这段代码 直接在子线程中更新了UI,却没有报错:
这是要打脸吗?但如果在子线程中加句线程休眠模拟耗时操作的话:
程序就崩溃了,报错如下:
翻译一下异常信息:只有创建这个view的线程才能操作这个view。限于篇幅,这里就不去跟源码了,直接说原因:
ViewRootImp 在 onCreate() 时还没创建;在 onResume()时,即ActivityThread的handleResumeActivity() 执行后才创建,调用 requestLayout(),走到 checkThread() 时就报错了。
可以打个日志简单的验证下:
加上休眠:
行吧,以后去面试别人问「子线程是不是一定不可以更新UI」别傻乎乎的说是了。
4.引生的另一个问题
说到「只能在主线程中更新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 = 201000; // 前台static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10; // 后台// ActivityManagerService.java → Broadcast广播、InputDispatching、ContentProviderstatic final int BROADCAST_FG_TIMEOUT = 101000; // 前台static final int BROADCAST_BG_TIMEOUT = 601000; // 后台static final int KEY_DISPATCHING_TIMEOUT = 51000; // 关键调度static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10*1000; // 内容提供者复制代码
时间统计区间:
起点:System_Server 进程调用 startProcessLocked 后调用 AMS.attachApplicationLocked()
终点:Provider 进程 installProvider及publishContentProviders 调用到 AMS.publishContentProviders()
超过这个时间,系统就会杀掉 Provider 进程。
0x2、Handler怎么用
1.sendMessage() + handleMessage()
代码示例如下:
黄色部分会有如下警告
Handler不是静态类可能引起「内存泄露」,原因以及正确写法等下再讲。另外,建议调用 Message.obtain() 函数来获取一个Message实例,为啥?点进源码:
从源码可以看到obtain()的逻辑:
1、判断Message池是否为空;
2、不为空,取出一个Message对象,池容量-1,返回;
3、否则,新建一个Message对象,返回;
这样可以「避免重复创建多个实例对象」节约内存,还有,Message池其实是一个「单链表结构」,定位到下述代码可以看到:池的容量为50
然后问题来了,Message信息什么时候加到池中?
当Message 被Looper分发完后,会调用 recycleUnchecked()函数,回收没有在使用的message对象。
如果你懂点数据结构的话,可以看出这是「单链表的头插法」
2.post(runnable)
代码示例如下:
跟下post():
实际上调用了 sendMessageDelayed() 发送消息,只不过延迟秒数为0,那Runnable是怎么变成Message的呢?跟下getPostMessage()
噢,获取一个新的Message示例后,把Runnable变量的值赋值给callback属性
3.附:其他两个种在子线程中更新UI的方法
activity.runOnUiThread()
view.post() 与 view.postDelay()
0x3、Handler底层原理解析
终于来到稍微有点技术含量的缓解,在观摩源码了解原理前,先说下几个涉及到的类。
1.涉及到的几个类
2.前戏
在我们使用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循环。
3.消息队列的运行
前戏过后,创建了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