Android的消息机制

Handler

在多线程的应用场景中,将工作线程中需更新UI的操作信息 传递到 UI主线程,从而实现 工作线程对UI的更新处理,最终实现异步消息的处理。多个线程并发更新UI的同时 保证线程安全

相关概念&源码解析

Android异步通信:手把手带你深入分析 Handler机制源码 - 简书

(1)Looper 循环器,从消息队列中取出消息,并分发给对应的Handler处理

  • 创建Looper对象主要通过方法:Looper.prepareMainLooper()【主线程】、Looper.prepare()【子线程】
  • 通过ThreadLocal存储Looper。它是线程内的数据存储类,在指定的线程中存储数据,也只能在指定的线程中获取数据
  • Looper.loop()开启消息循环,通过消息队列的next方法获取消息&发送给Handler处理
    msg.target.dispatchMessage(msg);
                // 把消息Message派发给消息对象msg的target属性
                // target属性实际是1个handler对象

在进行消息分发时(dispatchMessage(msg)),会进行1次发送方式的判断:

  1. msg.callback属性不为空,则代表使用了post(Runnable r)发送消息,则直接回调Runnable对象里复写的run()
  2. msg.callback属性为空,则代表使用了sendMessage(Message msg)发送消息,则回调复写的handleMessage(msg)
  • 主线程的消息循环不允许退出,即无限循环;子线程的消息循环允许退出:调用消息队列MessageQueue的quit()。 消息循环时个死循环,唯一跳出循环的方式时MessageQueue的next方法返回了null。Looper.quit()->MessageQueue.quit()->next返回null

(2)Message 消息,线程间通信的数据单元,即Handler发送和处理的对象

(3)MessageQueue 消息队列,存储Handler发送过来的消息,是一个单链表数据结构

next方法是个无线循环的方法

  • 创建循环器对象(Looper)的同时,会自动创建消息队列对象(MessageQueue)
  • 消息队列的next方法获取消息,如果没有消息,则阻塞

(4)Handler 处理者,主线程和子线程的通信媒介,在子线程中添加消息到消息队列,在主线程中处理Looper分发给它的消息

  • 在创建Handler对象的时候,绑定了Looper&messageQueue以及线程。Handler的消息处理会在绑定的线程中执行;绑定方式 = 先指定Looper对象,从而绑定了 Looper对象所绑定的线程(因为Looper对象本已绑定了对应线程);线程中必须要有Looper对象,否则无法创建Handler对象

发送消息到消息队列的方式共分为2种

Handler.sendMessage()

在发送消息的时候需要创建消息对象,通过Message.obtain()获取消息对象,应避免使用new重新分配内存。

发送消息的本质 = 为该消息定义target属性(即本身实例对象) & 将消息入队到绑定线程的消息队列中

Message内部维护了1个Message池,用于Message消息对象的复用
        // 使用obtain()则是直接从池内获取

Handler.post(Runnable)

在发送消息的时候,不需要创建消息对象。Runnable会被封装到msg中,再通过sendMessage(msg)

将 Runable对象 赋值给消息对象(message)的callback属性 m.callback = r

总之:两者的区别是

  1. 不需外部创建消息对象,而是内部根据传入的Runnable对象 封装消息对象
  2. 回调的消息处理方法是:复写Runnable对象的run()

线程(Thread)、循环器(Looper)、处理者(Handler)之间的对应关系如下:

  • 1个线程(Thread)只能绑定 1个循环器(Looper),但可以有多个处理者(Handler)
  • 1个循环器(Looper) 可绑定多个处理者(Handler)
  • 1个处理者(Handler) 只能绑定1个1个循环器(Looper)

通信机制的工作原理

在主线程中创建Looper和MessageQueue( 这个在ActivityThread的mian()函数中完成 ) ,并开启无线循环。在子线程中,通过Handler对象发送消息,消息放入消息队列中,Looper从消息队列取出消息,分发给对应的Handler处理(根据message.tag识别),最后在创建handler的线程(这里是主线程)处理message(回调handlerMessage()方法或者执行Runnable.run()方法)

Handler导致的内存泄露原因及其解决方案 

Android异步通信:详解 Handler 内存泄露的原因 - 简书

内存泄漏:

当对象不再被使用了,需要回收掉,但是被另外正在使用的对象引用,而不能被回收,导致了内存泄漏

Handler内存泄漏的原因:

Handler作为非static的内部类或者匿名内部类被创建,它默认持有外部类的引用,当外部类对象被销毁(activity生命周期结束),消息队列中的消息还没被处理完,这个时候消息仍持有handler引用,则间接也持有外部类引用,因而导致了内存泄漏。

注:主线程的消息队列生命周期跟整个应用保持一致,即生命周期长的持有了生命周期短的引用

解决办法

1、静态内部类 + 弱引用的方式(weakReference)

private Handler showhandler;

showhandler = new FHandler(this);

// 设置为:静态内部类
    private static class FHandler extends Handler{

        // 定义 弱引用实例
        private WeakReference<Activity> reference;

        // 在构造方法中传入需持有的Activity实例
        public FHandler(Activity activity) {
            // 使用WeakReference弱引用持有Activity实例
            reference = new WeakReference<Activity>(activity); }

        // 通过复写handlerMessage() 从而确定更新UI的操作
        @Override
        public void handleMessage(Message msg) {

2、当外部类结束生命周期时,清空Handler内消息队列

多线程的实现 

(1)一种是继承 Thread 类,另一种就是实现 Runnable 接口,都需要重写run方法

(2)AsycTask

封装了线程池和Handler,有两个线程池(一个用于任务排序,另一个真正执行任务)和一个Handler(用于子线程切换到主线程)

可以方便的执行后台任务以及在主线程中访问UI。但不适合特别耗时的后台任务(推荐线程池)

execute时串行执行任务,executeOnExecutor可以并行执行

onPreExecute():

doInBackground():在线程池中运行

onProcessUpdate(): 上一个方法调用publichProcess的时候,调用该方法

onPostExecute():任务被取消,该方法不会被执行,onCancelled会被执行

以上四个方法不能直接在程序中调用,AsycTask必须在主线程中创建,且excute也必须在主线程中调用,一个AsycTask对象只能执行一次execute

(3)HandlerThread

是一个能使用Handler的Thread

在run方法里创建了一个Looper并开启looper循环。因此在其中允许创建一个Handler

(4)IntentService

可以用于执行耗时的后台任务。封装了HandlerThread和Handler

IntentService第一次被启动,onCreate方法被调用,先创建一个HandlerThread,再使用它的Looper来构造一个Handler对象,最后发送的消息都会再HandlerThread中执行。

每次执行一个任务,都需要启动一次IntentService,onstartCommand方法被会被调用一次,onstartCommand会将外界的Intent交给onStart()处理,Intent对象被封装成消息发送,最后会被onHandlerIntent方法处理,该方法需要我们自己实现

线程的状态变化

要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有5种状态,即创建,就绪,运行,阻塞,终止。下面分别介绍一下这几种状态:

  • 创建状态 

在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread 类的构造方法来实现,例如 “Thread thread=new Thread()”。

  • 就绪状态 

新建线程对象后,调用该线程的 start() 方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。

  • 运行状态 

当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run() 方法。run() 方法定义该线程的操作和功能。

  • 阻塞状态 

一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让 CPU 暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(),suspend(),wait() 等方法,线程都将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

  • 死亡状态 

线程调用 stop() 方法时或 run() 方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

在此提出一个问题,Java 程序每次运行至少启动几个线程?

回答:至少启动两个线程,每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程,另外一个是垃圾收集线程。

 Message.obtain()怎么维护消息池的

消息在消息池中以单链表的形式存在,Message对象可以重复使用。同一时刻只能有一个线程能获取message对象,如果线程池为空,则new一个对象返回,否则会从池中获取。还能回收不再使用的message

postDealy后消息队列有什么变化,假设先 postDelay 10s, 再postDelay 1s, 怎么处理这2条消息

使用Handler的postDealy后消息队列可能会进行重新排序。消息队列里消息按执行先后时间进行排序,先执行的在前,后执行的在后。postDealy发送的消息会根据延迟时间与消息队列里存在的消息的执行时间进行比较,然后寻找插入位置插入消息

IdleHandler 

IdleHandler提供了一种在消息队列空闲时执行的某些操作的手段,适用于执行一些不重要且低优先级的任。消息队列通过一个ArrayList来储存添加的IdleHandler任务

同步屏障

postSyncBarrier发送的同步屏障消息,target == null,同步屏障消息会被过滤掉无法进入到消息队列

对于异步消息,Looper会遍历消息队列找到异步消息执行,确保像刷新屏幕等高优任务及时得到执行。同步消息得不到处理,这就是为什么叫同步屏障的原因。当使用了同步屏障,记得通过removeSyncBarrier移除,不然同步消息不能正常执行

主线程Looper一直循环查消息为什么没有卡主线程

Android中为什么主线程不会因为Looper.loop()里的死循环卡死? - 知乎

如果main方法中没有looper进行循环,那么主线程一运行完毕就会退出。即,ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的应用也就退出了

而且主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次释放Cpu资源进入睡眠。因此loop的循环并不会对CPU性能有过多的消耗

总结:Looer.loop()方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生ANR异常
便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程

消息队列的next方法

无线循环,没有消息会阻塞。若当前消息是同步屏障,从队列中取首个异步消息并执行;反之,则依次执行队列中的同步消息

Looper和Handler是否一定处于一个线程

不一定,两种方式,new mHandler()无参数的情况下需要在使用的线程中创建,new mHandler(线程的Looper)

子线程能不能更新UI

因为UI控件是线程不安全的,当多线程并发访问可能导致UI空间发生不可预期的情况,如果给UI加上锁,会让程序变得复杂和低效

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值