换个姿势,带着问题看 Handler,android 应用程序开发的流程
答:Android Framework 架构中的一个 基础组件,用于 子线程与主线程间的通讯,实现了一种 非堵塞的消息传递机制。
2.Handler 有什么用
答:把子线程中的 UI 更新信息传递 给主线程(UI 线程),以此完成 UI 更新操作。
3.为什么要用 Handler,不用行不行
答:不行,Handler 是 android 在设计之初就封装的 一套消息创建、传递、处理机制。
Android 要求:在主线程(UI 线程)中更新 UI,注意,是要求,并不是规定,你不听,硬是要:在子线程中更新 UI,也是可以的,比如在子线程中创建 Dialog:
运行后:
没有报错,对话框正常弹出,而我们平时在 子线程中更新 UI 的报错信息是这样的:
异常翻译:只有创建视图层次结构的原始线程才能触摸其视图;
引起原因:在子线程中直接更新主线程创建的 UI;
也就是说:子线程更新 UI 也行,但是只能更新子线程创建的 View;
换句话说:Android 的 UI 更新(GUI)被设计成了单线程;
你可能会问,为啥不设计成多线程?
答:多个线程同时对同一个 UI 控件进行更新,容易发生不可控的错误!
那么怎么解决这种线程安全问题?
答:最简单的处理方式——加锁,不是加一个,是每层都要加锁(用户代码→GUI 顶层→GUI 底层...)但这样也意味着更多的 耗时,从而导致 UI 更新效率降低,界面卡顿等。
而如果每一层共用一把锁的话,其实就是 单线程,所以,最后的结论是:
Android 没有采用「线程锁」,而是采用「单线程消息队列机制」,实现了一个「伪锁」
这个疑问解决了,再说一个网上很常见的主线程更新 UI 的例子:
上面这段代码 直接在子线程中更新了 UI,却没有报错:
打脸?莫慌,在子线程代码块中加一句休眠模拟耗时操作:
程序就崩溃了,报错日志如下:
前面说了 Android 的 UI 更新被设计成单线程,但为何在添加休眠后才报错,限于篇幅,不跟源码,直接说原因:
ViewRootImp
在onCreate()
调用时还没创建;在
onResume()
时,即 **ActivityThread.handleResumeActivity()
**执行后才创建;调用
View.requestLayout()
,最终调到ViewRootImpl.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 = 20*1000; // 前台 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 = 101000; // 内容提供者
时间统计区间:
起点:
System_Server
进程调用startProcessLocked()
后调用AMS.attachApplicationLocked()
终点:
Provider
进程的 **installProvider()
**及publishContentProviders()
调用到AMS.publishContentProviders()
超过这个时间,系统就会杀掉 Provider 进程。
0x2、Handler 怎么用
1.sendMessage() + handleMessage()
代码示例如下:
黄色部分会有如下警告
Handler 不是静态类可能引起**内存泄露
**,原因以及正确写法等下再讲。
另外,建议调用 Message.obtain() 函数来获取一个 Message 实例,为啥?点进源码:
从源码,可以看到 obtain()的逻辑:加锁 → 判断 Message 池是否为空
① 不为空,取一枚 Message 对象,正在使用标记置为 0,池容量-1,返回此对象;
② 为空,新建一个 Message 对象返回;
此处复用 Message,可避免**避免重复创建实例对象
,达到节约内存的目的,而且不难看出 Message 实际上是无头结点的单链表
**。
上述获取消息池的逻辑:
定位到下述代码,还可以知道:池的容量为 50
然后问题来了,Message 信息什么时候加到池中?
答:当 Message 被 Looper 分发完后,会调用 recycleUnchecked()函数,回收没有在使用的 Message 对象。
标志设置为**FLAG_IN_USE
**,表示正在使用,相关属性重置,加锁,判断消息池是否满,未满,单链表头插法 将消息插入到表头。
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 函数
定位到:Looper → prepareMainLooper 函数
定位到:Looper → prepare 函数
定位到:Looper → Looper 构造函数
另外这里的 mQuitAllowed 变量,直译「退出允许」,具体作用是?跟下 MessageQueue:
em...用来 防止开发者手动终止消息队列,停止 Looper 循环。
3.消息队列的运行
前戏过后,创建了 Looper 与 MessageQueue 对象,接着调用 Looper.loop()开启轮询。 定位到:Looper → loop 函数
接着有几个问题,先是这个 myLooper() 函数:
这里的 ThreadLocal → 线程局部变量 → JDK 提供的用于解决线程安全的工具类。 作用:为每个线程提供一个独立的变量副本 → 以解决并发访问的冲突问题。 本质:
每个 Thread 内部都维护了一个 ThreadLocalMap,这个 map 的 key 是 ThreadLocal,
value 是 set 的那个值。get 的时候,线程都是从自己的变量中取值,所以不存在线程安全问题。
主线程和子线程的 Looper 对象实例相互隔离的!!!
另外,线程为 key 也保证了每个线程只有一个 Looper,而创建 Looper 对象时又会创建 MessageQueue 对象,所以间接保证每个线程最多只能有一个 MessageQueue。
知道这个以后,有个问题就解惑了:
为什么子线程中不能直接 new Handler(),而主线程可以?
答:主线程与子线程不共享同一个 Looper 实例,主线程的 Looper 在启动时就通过 prepareMainLooper() 完成了初始化,而子线程还需要调用 Looper.prepare() 和 Looper.loop()开启轮询,否则会报错,不信,可以试试:
直接就奔溃了~
加上试试?
可以,没有报错,程序正常运行。
对了,既然说 Handler 用于子线程和主线程通信,试试在主线程中给子线程的 Handler 发送信息,修改一波代码:
运行,直接报错:
原因:多线程并发的问题,当主线程执行到 sendEnptyMessage 时,子线程的 Handler 还没有创建。 一个简单的解决方法是:主线程延时给子线程发消息,修改后的代码示例如下:
运行结果如下:
可以,不过其实 Android 已经给我们封装好了一个轻量级的异步类 HandlerThread
4.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{Thread.currentThread()}")lock.notifyAll()}
fun consume() = synchronized(lock) {while(items<=0) {lock.wait()}Thread.sleep(rand.nextInt(100).toLong())items--println("Consumed, count is{Thread.currentThread()}")lock.notifyAll()}
5.当我们用 Handler 发送一个消息发生了什么?
扯得有点远了,拉回来,刚讲到 ActivityThread 在 main 函数中调用 **Looper
.prepareMainLooper** 完成主线程 Looper 初始化,然后调用 Looper.loop() 开启消息循环 等待接收消息。
嗯,接着说下 发送消息,上面说了,Handler 可以通过 sendMessage()和 post() 发送消息,上面也说了,源码中,这两个最后调用的其实都是 sendMessageDelayed()完成的:
第二个参数:当前系统时间+延时时间,这个会影响「调度顺序」,跟 sendMessageAtTime()
获取当前线程 Looper 中的 MessageQueue 队列,判空,空打印异常,否则返回 enqueueMessage(),跟:
这里的 mAsynchronous 是 异步消息的标志,如果 Handler 构造方法不传入这个参数,默认 false: 这里涉及到了一个「同步屏障」的东西,等等再讲,跟:MessageQueue -> enqueueMessage
如果你了解数据结构中的单链表的话,这些都很简单。 不了解的可以移步至【面试】数据结构与算法(二) 学习一波~
评论