换个姿式,带着问题看Handler

Handler,老生常谈,网上关于它的文章可谓是“泛滥成灾”,不过实际开发中用得很少。
毕竟,如今写异步,RxAndroid链式调用Kotlin协程同步方式写异步代码,不香么?
不过,面试官仍是喜欢章口就莱一句:java

固然,应对方法也很简单,找一篇《…Handler详解》之类的文章,背熟便可~
不过,对于我这种好刨根问底的人来讲,本身过一遍源码心理才踏实,
并且,我发现「带着问题」看源码,思考理解本质,印象更深,收获更多,遂有此文。android

罗列下本文说起的问题,若有答不出的可按需阅读本文,谢谢~web

  • 一、Handler问题三连:是什么?有什么用?为何要用Handler,不用行不行?
  • 二、真的只能在主(UI)线程中更新UI吗?
  • 三、真的不能在主(UI)线程中执行网络操做吗?
  • 四、Handler怎么用?
  • 五、为何建议使用Message.obtain()来建立Message实例?
  • 六、为何子线程中不能够直接new Handler()而主线程中能够?
  • 七、主线程给子线程的Handler发送消息怎么写?
  • 八、HandlerThread实现的核心原理?
  • 九、当你用Handler发送一个Message,发生了什么?
  • 十、Looper是怎么拣队列里的消息的?
  • 十一、分发给Handler的消息是怎么处理的?
  • 十二、IdleHandler是什么?
  • 1三、Looper在主线程中死循环,为啥不会ANR?
  • 1四、Handler泄露的缘由及正确写法
  • 1五、Handler中的同步屏障机制

0x一、Handler问题三连


1.Handler是什么


答:Android定义的一套 子线程与主线程间通信消息传递机制面试


2.Handler有什么用

答:把子线程中的 UI更新信息传递 给主线程(UI线程),以此完成UI更新操做。算法


3.为何要用Handler,不用行不行


答:不行,由于android在设计之初就封装了一套消息建立、传递、处理。若是不遵循就不能更新UI信息,就会报出异常(异步消息处理异常)数组

在Android中,为了提升系统运行效率,没有采用「线程锁」,带来了:安全

多个线程并发更新UI时的线程安全问题网络

为了安全保证UI操做是线程安全的,规定数据结构

只能在主线程(UI线程)中完成UI更新多线程

但,真的只能在UI线程中更新UI吗?

上面这段代码 直接在子线程中更新了UI,却没有报错:

这是要打脸吗?但若是在子线程中加句线程休眠模拟耗时操做的话:

程序就崩溃了,报错以下:

翻译一下异常信息:只有建立这个view的线程才能操做这个view。限于篇幅,这里就不去跟源码了,直接说缘由:

ViewRootImponCreate() 时还没建立;
onResume()时,即ActivityThreadhandleResumeActivity() 执 行后才建立,
调用 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、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 进程 installProviderpublishContentProviders 调用到 AMS.publishContentProviders()
  • 超过这个时间,系统就会杀掉 Provider 进程。

0x二、Handler怎么用


1.sendMessage() + handleMessage()


代码示例以下

黄色部分会有以下警告

Handler不是静态类可能引发「内存泄露」,缘由以及正确写法等下再讲。
另外,建议调用 Message.obtain() 函数来获取一个Message实例,为啥?点进源码:

从源码能够看到obtain()的逻辑:

  • 一、判断Message池是否为空;
  • 二、不为空,取出一个Message对象,池容量-1,返回;
  • 三、不然,新建一个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()


0x三、Handler底层原理解析


终于来到稍微有点技术含量的环节,在观摩源码了解原理前,先说下几个涉及到的类。


1.涉及到的几个类



2.前戏


在咱们使用Handler前,Android系统已为咱们作了一系列的工做,其中就包括了

建立「Looper」和「MessageQueue」对象

上图中有写:ActivityThreadmain函数是APP进程的入口,定位到 ActivityThread → main函数

定位到:Looper → prepareMainLooper函数

定位到:Looper → prepare函数

定位到:Looper → Looper构造函数

另外这里的 quitAllowed 变量,直译「退出容许」,具体做用是?跟下 MessageQueue

em…用来 防止开发者手动终止消息队列,中止Looper循环


3.消息队列的运行


前戏事后,建立了Looper与MessageQueue对象,接着调用Looper.loop()开启轮询。
定位到:Looper → loop函数

接着有几个问题,先是这个 myLooper() 函数:

这里的 ThreadLocal线程局部变量JDK提供的用于解决线程安全的工具类
做用为每一个线程提供一个独立的变量副本以解决并发访问的冲突问题
本质

每一个Thread内部都维护了一个ThreadLocalMap,这个map的key是ThreadLocal,
value是set的那个值。get的时候,都是从本身的变量中取值,因此不存在线程安全问题。

主线程和子线程的Looper对象实例相互隔离的!!!
意味着:主线程和子线程Looper不是同一个!!!

知道这个之后,有个问题就解惑了:

为何子线程中不能直接 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$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()
}
复制代码

5.当咱们用Handler发送一个消息发生了什么?


扯得有点远了,拉回来,刚讲到 ActivityThreadmain函数中调用 Looper.prepareMainLooper
完成主线程 Looper初始化,而后调用 Looper.loop() 开启消息循环 等待接收消息

嗯,接着说下 发送消息,上面也说了,Handler能够经过sendMessage()和 post() 发送消息,
上面也说了,源码中,这两个最后调用的其实都是 sendMessageDelayed()完成的:

第二个参数:当前系统时间+延时时间,这个会影响「调度顺序」,跟 sendMessageAtTime()

获取当前线程Looper中的MessageQueue队列,判空,空打印异常,不然返回 enqueueMessage(),跟:

这里的 mAsynchronous异步消息的标志,若是Handler构造方法不传入这个参数,默认false:
这里涉及到了一个「同步屏障」的东西,等等再讲,跟:MessageQueue -> enqueueMessage

若是你了解数据结构中的单链表的话,这些都很简单。
不了解的能够移步至【面试】数据结构与算法(二) 学习一波~


6.Looper是怎么拣队列的消息的?


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资源,有消息时唤醒线程;


7.分发给Handler的消息是怎么处理的?


经过MessageQueuequeue.next()拣出消息后,调用msg.target.dispatchMessage(msg)
把消息分发给对应的Handler,跟到:Handler -> dispatchMessage

到此,关于Handler的基本原理也说的七七八八了~


8.IdleHandler是什么?


评论区有小伙子说:把idleHandler加上就完整了,那就安排下吧~
MessageQueue 类中有一个 static 的接口 IdleHanlder

翻译下注释:当线程将要进入堵塞,以等待更多消息时,会回调这个接口;
简单点说:当MessageQueue中无可处理的Message时回调
做用:UI线程处理完全部View事务后,回调一些额外的操做,且不会堵塞主进程;

接口中只有一个 queueIdle() 函数,线程进入堵塞时执行的额外操做能够写这里,
返回值是true的话,执行完此方法后还会保留这个IdleHandler,不然删除。

使用方法也很简单,代码示例以下:

输出结果以下

看下源码,了解下具体的原理:MessageQueue,定义了一个IdleHandler的列表和数组

定义了添加和删除IdleHandler的函数:

next() 函数中用到了 mIdleHandlers 列表:

原理就这样,通常使用场景:绘制完成回调,例子可参见:
《你知道 android 的 MessageQueue.IdleHandler 吗?》
也能够在一些开源项目上看到IdleHandler的应用:
useof.org/java-open-s…


0x四、一些其余问题


1.Looper在主线程中死循环,为啥不会ANR?

答:上面说了,Looper经过queue.next()获取消息队列消息,当队列为空,会堵塞,
此时主线程也堵塞在这里,好处是:main函数没法退出,APP不会一启动就结束!

你可能会问:主线程都堵住了,怎么响应用户操做和回调Activity声明周期相关的方法?

答:application启动时,可不止一个main线程,还有其余两个Binder线程ApplicationThreadActivityManagerProxy,用来和系统进程进行通讯操做,接收系统进程发送的通知。

  • 当系统受到因用户操做产生的通知时,会经过 Binder 方式跨进程通知 ApplicationThread;
  • 它经过Handler机制,往 ActivityThreadMessageQueue 中插入消息,唤醒了主线程;
  • queue.next() 能拿到消息了,而后 dispatchMessage 完成事件分发;
    PS:ActivityThread 中的内部类H中有具体实现

死循环不会ANR,可是 dispatchMessage 中又可能会ANR哦!若是你在此执行一些耗时操做
致使这个消息一直没处理完,后面又接收到了不少消息,堆积太多,从而引发ANR异常!!!


2.Handler泄露的缘由及正确写法


上面说了,若是直接在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) {
                0 -> {
                    activity.notifyUI()
                }
            }
        }
    }
}
复制代码

3.同步屏障机制


经过上面的学习,咱们知道用Handler发送的Message后,MessageQueueenqueueMessage()
按照 时间戳升序 将消息插入到队列中,而Looper则按照顺序,每次取出一枚Message进行分发,
一个处理完到下一个。这时候,问题来了:有一个紧急的Message须要优先处理怎么破
你可能或说直接sendMessage()不就能够了,不用等待立马执行,看上去说得过去,不过可能
有这样一个状况:

一个Message分发给Handler后,执行了耗时操做,后面一堆本该到点执行的Message在那里等着,这个时候你sendMessage(),仍是得排在这堆Message后,等他们执行完,再到你!

对吧?Handler中加入了「同步屏障」这种机制,来实现「异步消息优先执行」的功能。

添加一个异步消息的方法很简单:

  • 一、Handler构造方法中传入async参数,设置为true,使用此Handler添加的Message都是异步的;
  • 二、建立Message对象时,直接调用setAsynchronous(true)

通常状况下:同步消息和异步消息没太大差异,但仅限于开启同步屏障以前。
能够经过 MessageQueuepostSyncBarrier 函数来开启同步屏障:

行吧,这一步简单的说就是:往消息队列合适的位置插入了同步屏障类型的Message (target属性为null)
接着,在 MessageQueue 执行到 next() 函数时:

遇到target为null的Message,说明是同步屏障,循环遍历找出一条异步消息,而后处理。
在同步屏障没移除前,只会处理异步消息,处理完全部的异步消息后,就会处于堵塞
若是想恢复处理同步消息,须要调用 removeSyncBarrier() 移除同步屏障:

在API 28的版本中,postSyncBarrier()已被标注hide,但依旧可在系统源码中找到相关应用,好比:
为了更快地响应UI刷新事件,在ViewRootImplscheduleTraversals函数中就用到了同步屏障:


参考文献


相关文章
相关标签/搜索