Android消息机制(Handler)

在Android开发的过程中,我们常常会将耗时的一些操作放在子线程(work thread)中去执行,然后将执行的结果告诉UI线程(main thread)。Handler不仅仅能将子线程的数据传递给主线程,它能实现任意两个线程的数据传递。

为什么系统不允许子线程访问UI呢,因为AndroidUI不是线程安全的,如果在多线程操控UI可能会导致UI控件处于不可预知的状态;那为什么系统不给UI控件的访问加锁呢?首先上锁机制会让UI访问的逻辑变得复杂,其次加锁机制会降低访问效率。因此,安卓之所以提供Handler是为了解决在子线程不能更新UI的矛盾。

Handler 架构图

image.png

为什么要采用Handler机制

答: 在Android中,只有主线程才能更新UI,但是主线程不能进行耗时操作,否则会产生ANR异常,所以常常把耗时操作放到其他子线程进程。如果在子线程中需要更新UI,一般都是通过Handler发送消息,主线接受消息并进行相应的逻辑处理。

Handler用途

Handler有两个主要的用途

1 安排消息和Runnable对象在未来执行
2 将你的一个动作放在不同的线程上执行

Handler模型

Handler的消息机制主要包含:

  • Message:消息实体
  • MessageQueue:消息队列
  • Handler:消息管理类向消息池发送各种消息事件
  • Looper:不断的循环执行(Looper.loop),按分发机制将消息分发给目标处理者

1.一个线程有几个 Handler?

答:一个线程可以用有多 Handler,因为 Handler 最终是被 Message 持用的(post 里面的 Runnable 最终也会被包装成一个 Message),以便 Looper 在拿到 Message 后调用 Handler 的 dispatchMessage 完成回调,而且项目中仔细去看也确实如此,我们可以每个 Activity 中都创建一个 Handler 来处理回调到主线程的任务。

2.一个线程有几个 Looper?如何保证?

答:一个线程只能拥有一个 Looper,这里从源码中就可以看到,sThreadLocal.set 只调用了一次,如果再次调用 prepare 会判断 sThreadLocal.get 是否为空,如果不为空就直接抛出异常了,也就是同一线程多次调用 prepare 方法会直接崩溃,这里也是避免了程序去修改某个线程已经设置好的 Looper 值。

image

补充:ThreadLocal 提供线程局部变量,也就是对应值只有该线程支持,并不会多线程共享。那它是如何做到线程局部变量这个效果的呢?它内部靠的是 ThreadLocalMap,线程作为 key,值作为 value,这样我去取对应值的时候,其实通过线程 Key 拿去对应的 value,这样就保证了值是当前线程独享的。

3.为何主线程可以使用 Handler?如果想要在子线程中使用 Handler 机制要做些什么准备?

答:先来看下面的源码,Handler 的构造中(无论调用哪个最终都会走到这里),是需要判断当前线程是否存在 Looper 的,如果不存在会直接抛出异常,主线程之所以可以使用 Handler 是因为系统帮在 ActivityThread 中已经帮我们创建了 Looper 并且已经让它运行了起来。

image

系统帮我们在主线程创建 Looper 的代码:

image

如果我们现在子线程中使用 Handler 的话,之需要模仿系统怎么创建 Looper 即可,其实就是两步,在子线程中调用 Looper.prepare() 和 Looper.loop() 即可,prepare 帮我们在对应线程创建 Looper,loop 让刚刚创建好的 Looper 运行起来。

这两步完成后我们就可以在子线程中使用 Handler 了。

以上所说的 Handler 使用指的是 Handler 的创建,比如在 A 线程创建后就可以在任何位置使用了,也就是在任意线程发送消息,然后在 A 线程处理消息。

4.既然可以存在多个 Handler 往 MessageQueue 中添加数据(发消息时各个 Handler 可能处于不同线程),那它内部是如何确保线程安全的?

答:这里主要关注 MessageQueue 的消息存取即可,看源码内部的话,在往消息队列里面存储消息时,会拿当前的 MessageQueue 对象作为锁对象,这样通过加锁就可以确保操作的原子性和可见性了。

消息的读取也是同理,也会拿当前的 MessageQueue 对象作为锁对象,来保证多线程读写的一个安全性。

image

5.我们使用 Message 时应该如何创建它?

答:创建的它的方式有两种,一种是直接 new 一个 Message 对象,另一种是通过调用 Message.obtain() 的方式去复用一个已经被回收的 Message,当然日常使用者是推荐使用后者来拿到一个 Message,因为不断的去创建新对象的话,可能会导致垃圾回收区域中新生代被占满,从而触发 GC。

Message 中的 sPool 就是用来存放被回收的 Message,当我们调用 obtain 后,会先查看是否有可复用的对象,如果真的没有才会去创建一个新的 Message 对象。

补充:主要的 Message 回收时机是:

  • 在 MQ 中 remove Message 后;
  • 单次 loop 结束后;
  • 我们主动调用 Message 的 recycle 方法后;

6.Message 的数据结构是什么样子?

答:单链表,Message 中会通过 next 来持有下一个 Message 对象的引用,这是一个典型的链表结构。

image

其实文中写到的这些问题并不只是问到这里就结束了,很多问题都可以深挖,尤其是这个问题,看着很短,接下去问很多关于链表的其他问题了,比如链表反转,或是延伸到其他数据结构问题。

7.主线程 Looper 与子线程 Looper 有什么不同?

答:最主要的区别还在在于 Looper 的 loop 循环是否能够退出,主线程创建时传入的 quitAllowed 是 false。

image

Looper.loop 这个方法在拿到的消息为空时就会退出那个死循环,不过一般是不为空的,哪怕没有消息最多也是阻塞,只有调用 Looper.quit 时才会在消息队列清空消息并把消息设置为 null。

那 loop 的死循环结束意味着什么呢?看下面 ActivityThread 的代码就知道了,如果主线程 Looper 结束就说明程序也要退出了,因为只有 loop 不断执行才不会走到抛出异常那一行。

文件:android/app/ActivityThread.java

    public static void main(String[] args) {
        //  ...
        //  主线程 Looper 创建
        Looper.prepareMainLooper();
        //  ...
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        //  ...
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

8. Looper 死循环为什么不会导致应用卡死?

真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死

答:epoll+pipe,有消息就依次执行,没消息就block住,让出CPU,等有消息了,epoll会往pipe中写一个字符,把主线程从block状态唤起,主线程就继续依次执行消息。

简答:loop死循环不造成ui阻塞 是因为当消息队列无消息时会进入休眠状态 这时候会释放cpu资源,有消息时会被唤醒。

主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

9. 子线程有哪些更新UI的方法。

  • 主线程中定义Handler,子线程通过mHandler发送消息,主线程Handler的handleMessage更新UI。
  • 用Activity对象的runOnUiThread方法。
  • 创建Handler,传入getMainLooper。
  • View.post(Runnable r) 。

10. 如何处理Handler 使用不当导致的内存泄露?

解决Handler内存泄露主要2点

  1. 有延时消息,要在Activity销毁的时候移除Messages
  2. 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity使用弱引用。

11. 消息分发的优先级:

  • Message的回调方法:message.callback.run(),优先级最高;
  • Handler的回调方法:Handler.mCallback.handleMessage(msg),优先级仅次于1;
  • Handler的默认方法:Handler.handleMessage(msg),优先级最低。

12. 消息池

在代码中,可能经常看到recycle()方法,咋一看,可能是在做虚拟机的gc()相关的工作,其实不然,这是用于把消息加入到消息池的作用。这样的好处是,当消息池不为空时,可以直接从消息池中获取Message对象,而不是直接创建,提高效率。

静态变量sPool的数据类型为Message,通过next成员变量,维护一个消息池;静态变量MAX_POOL_SIZE代表消息池的可用大小;消息池的默认大小为50。

消息池常用的操作方法是obtain()和recycle()。

13. Handler如何切换线程的

切换线程实际上就是调用位置的切换而已,Handler切换线程的机制最终原因是handleMessage方法的调用位置的切换,比如主线程。

handler将自己的引用间接被Looper持有,当Looper在主线程调用loop()方法时,该方法会取出handler并调用其handleMessage()方法,相当于切换到主线程

MessageQueue内部使用单链表来存储信息Message,Handler发送的Message全部都添加到了MessageQueue中,Looper#loop()方法通过调用MessageQueue#next()方法不断遍历链表中的Message,当取得符合的Message后,通过Message持有的Handler对象引用调用Handler#handleMessage方法,如此,Looper#loop()在哪个线程调用的,handleMessage方法就切换到哪个线程了。

14. 延时消息如何处理

1.消息是通过MessageQueen中的enqueueMessage()方法加入消息队列中的,并且它在放入中就进行好排序,链表头的延迟时间小,尾部延迟时间最大

2.Looper.loop()通过MessageQueue中的next()去取消息

3.next()中如果当前链表头部消息是延迟消息,则根据延迟时间进行消息队列会阻塞,不返回给Looper message,知道时间到了,返回给message

4.如果在阻塞中有新的消息插入到链表头部则唤醒线程

5.Looper将新消息交给回调给handler中的handleMessage后,继续调用MessageQueen的next()方法,如果刚刚的延迟消息还是时间未到,则计算时间继续阻塞

Handler总结

在子线程中,如果手动为其创建Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。(【 Looper.myLooper().quit(); 】)

Android 的消息机制主要指 Handler 的运行机制,以及 Handler 所附带的 MessageQueue 和 Looper 的工作过程,三者是一个整体。当我们要将任务切换到某个指定的线程(如 UI 线程)中执行的时候,会通过 Handler 的 send(Message message msg) 和 post(Runnable r) 进行消息的发送,post()方法最终也是通过 send() 方法来完成的。

发送的消息会插入到 MessageQueue 中(MessageQueue 虽然叫做消息队列,但是它的内部实现并不是队列,而是单链表,因为单链表在插入和删除上比较有优势),然后 Looper 通过 loop() 方法进行无限循环,判断 MessageQueue 是否有新的消息,有的话就立刻进行处理,否则就一直阻塞在那里,loop() 跳出无限循环的唯一条件是 MessageQueue 返回 null。

Looper 将处理后的消息交给 Handler 进行处理,然后 Handler 就进入了处理消息的阶段,此时便将任务切换到 Handler 所在的线程,我们的目的也就达到了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容