• 全国 [切换]
  • 二维码
    富博农业

    手机WAP版

    手机也能找商机,信息同步6大终端平台!

    微信小程序

    微信公众号

    当前位置: 首页 » 行业新闻 » 热点新闻 » 正文

    Handler和ThreadLocal的原理解析

    放大字体  缩小字体 发布日期:2020-03-02 12:54:15   浏览次数:1156  发布人:5455****  IP:106.39.55.***  评论:0
    导读

    前言    说到Handler机制不得不提的就是这几大将:Handler,Looper,MessageQueue,Message。延伸重点ThreadLocal!!!    UI主线程初始化第一个Handler时会通过ThreadLocal创建一个Looper,该Looper与UI主线程一一对应。使用ThreadLocal的目的是保证每一个线程只创建唯一一个Looper。之后其他Handler初始

    前言

        说到Handler机制不得不提的就是这几大将:Handler,Looper,MessageQueue,Message。延伸重点ThreadLocal!!!
        UI主线程初始化第一个Handler时会通过ThreadLocal创建一个Looper,该Looper与UI主线程一一对应。使用ThreadLocal的目的是保证每一个线程只创建唯一一个Looper。之后其他Handler初始化的时候直接获取第一个Handler创建的Looper。Looper初始化的时候会创建一个消息队列MessageQueue。至此,主线程、消息循环、消息队列之间的关系是1:1:1。Handler、Looper、MessageQueue的初始化流程如下图所示:Hander持有对UI主线程消息队列MessageQueue和消息循环Looper的引用,子线程可以通过Handler将消息发送到UI线程的消息队列MessageQueue中。






        我们写代码时主线程为啥不用初始化looper?因为主线程looper早在ActivityThread初始化的时候就声明好了,可以直接拿来用的。分析源码我们知道MessageQueue在Looper中,Looper初始化后作为对象丢给了Handler,并且又存在了ThreadLocal里面,ThreadLocal对象的话作为Key又存在了ThreadLocalMap,ThreadLocalMap对象是Thread里面的一个属性值,也就是说Looper作为桥梁连接了Handler与Looper所在的线程,下面我们细品一下代码。

    要想深入理解Handler的机制,我们的需要需理解ThreadLocal:

        ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

    ThreadLocal的原理

        要看原理那么就得从源码看起。
        我们先从set方法看起:






        从上面的代码不难看出,ThreadLocal  set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map中属性不为空,则直接更新value值,如果map中找不到此ThreadLocal对象,则在threadLocalMap创建一个,并将value值初始化。显然ThreadLocal对象存的值是根据线程走的!

        那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。

    首先第一步我们得要知道这个东西






    每个Thread有一个属性,类型是ThreadLocalMap,从代码不难看出ThreadLocalMap是ThreadLocal的内部静态类。它是与线程所绑定联系在一起的,可以看成一个线程只有一个ThreadLocalMap,知道这一点我们再往下看。






    ThreadLocalMap的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal对象作为key,使用我们设置的value作为value。






    get的话就比较简单了就是获取当前线程的ThreadLocalMap属性值,在获取Map中对应ThreadLocal对象的value值并返回。

       ThreadLocal总结一下就是:每个线程Thread自身有一个属性ThreadLocalMap,然后ThreadLocalMap是一个键值对,它的key值是ThreadLocal对象,它的value值则是我们想要保存处理的数据值。getMap是找到对应线程的ThreadLocalMap属性值,然后通过判断可以初始化或者更新数值。






    ThreadLocal分析完了我们接着来看Handler吧。

       因为主线程在ActivityThread的main方法中已经创建了Looper,所以主线程使用Handler时可以直接new;子线程使用Handler时需要调用Looper的prepare和loop方法才能进行使用,否则会抛出异常。所以我们从Looper的prepare来分析。




    prepare分析

        Looper 提供了 Looper.prepare() 方法来创建 Looper ,并且会借助 ThreadLocal 来实现与当前线程的绑定功能。Looper.loop() 则会开始不断尝试从 MessageQueue 中获取 Message , 并分发给对应的 Handler,也就是说 Handler 跟线程的关联是靠 Looper 来实现的。

    Looper.loop() 负责对消息的分发,也是和prepare配套使用的方法,两者缺一不可。




    loop分析1




    loop分析2

    msg.target是个啥呢,我们追到Message里面不难发现其实它就是我们发送消息的Handler,这写法是不是很聪明,当从MessageQueen中捞出Message后,我们就能直接调用Handler的dispatchMessage,然后就会走到我们的Handler的handleMessage了。直接上源码:











        处理分析完了,我们看个简单点的,消息发送吧。Handler 提供了一些列的方法让我们来发送消息,如 send()系列 post()系列 。不过不管我们调用什么方法,最终都会走到 MessageQueue的enqueueMessage(Message,long) 方法。也就是实现了消息的发送,将Message插入到我们的MessageQueue中。

    注意:dispatchMessage() 方法针对 Runnable 的方法做了特殊处理,如果是 ,则会直接执行 Runnable.run() 。(判断依据是上述msg.callback !=null这句)

     MessageQueue是个单链表。

    MessageQueue里消息按时间排序

    MessageQueue的next()是个堵塞方法

    总结分析:Looper.loop() 是个死循环,会不断调用 MessageQueue.next() 获取 Message ,并调用 msg.target.dispatchMessage(msg) 回到了 Handler 来分发消息,以此来完成消息的回调。


    Handler什么会出现内存泄漏问题呢?

    分析

        Handler使用是用来进行线程间通信的,所以新开启的线程是会持有Handler引用的,如果在Activity等中创建Handler,并且是非静态内部类的形式,就有可能造成内存泄漏。

        非静态内部类是会隐式持有外部类的引用,所以当其他线程持有了该Handler,线程没有被销毁,则意味着Activity会一直被Handler持有引用而无法导致回收。

    MessageQueue中如果存在未处理完的Message,Message的target也是对Activity等的持有引用,也会造成内存泄漏。

    解决的办法

        使用静态内部类 + 弱引用的方式: 静态内部类不会持有外部类的的引用,当需要引用外部类相关操作时,可以通过弱引用还获取到外部类相关操作,弱引用是不会造成对象该回收回收不掉的问题,不清楚的可以查阅JAVA的几种引用方式的详细说明。

        在外部类对象被销毁时,将MessageQueue中的消息清空。

    Looper死循环为什么不会导致应用卡死,会消耗大量资源吗?

        对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。

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

    不推荐电脑端访问(用户少)
    请微信扫一扫,查看完整信息(记得微信收藏)
     
    (文/匿名(若涉版权问题请联系我们核实发布者) / 非法信息举报 / 删稿)
    打赏
    免责声明
    • 
    本文为昵称为 5455**** 发布的作品,本文仅代表发布者个人观点,本站未对其内容进行核实,请读者仅做参考,如若文中涉及有违公德、触犯法律的内容,一经发现,立即删除,发布者需自行承担相应责任。涉及到版权或其他问题,请及时联系我们154208694@qq.com删除,我们积极做(权利人与发布者之间的调停者)中立处理。郑重说明:不 违规举报 视为放弃权利,本站不承担任何责任!
    有个别老鼠屎以营利为目的遇到侵权情况但不联系本站或自己发布违规信息然后直接向本站索取高额赔偿等情况,本站一概以诈骗报警处理,曾经有1例诈骗分子已经绳之以法,本站本着公平公正的原则,若遇 违规举报 我们100%在3个工作日内处理!
    0 条相关评论
     

    福博农产品采购网 (c)2008-现在 foubo.com All Rights Reserved.