Android线程通信,Handler+Looper+Message机制解析

背景

对于任何需要直接用户交互的产品来讲,及时的用户响应都是良好用户体验的先决条件,借由现代分时操作系统时间片轮转的思路,将UI交互与后台耗时操作进行分离,也成为了Android应用设计的基本要求。利用JAVA多线程技术满足上述需求的同时也带来了新的问题,那就是如何保证多个线程有效的通信。也即是Handler+Looper+Message设计的初衷,一套线程间通信机制。

如果我们将两个线程想象成两个非常忙碌的公司员工的话,我们的问题可以转述为如何设计一套良好的邮件系统,可以保证两个员工最大化的工作效率。这样来看的话,我们的邮件系统要正常运作,至少需要这些元素:符合规范的邮件实体,能够帮助员工收发邮件的邮件客户端,一个能够中转邮件的邮箱,还有能够识别这个员工的一组用户名和密码。而如果这两位员工不巧笨到只会一行行的执行机器指令,为了最大化员工的效率,最好还能够在邮件里或者在客户端里明确告诉这个员工,收到邮件了该怎么办(即所谓的回调)。Android 系统的设计者们就充分考虑了这几个方面,并把这些元素凝结为了下面的几个类。

邮件——Message

邮件是一个邮件系统最基本和最核心的元素,是数据的承载者。Message类在我们的系统中就承担着这个角色。
Message的数据部分主要包括以下几部分:

int what,arg0,arg1;

这三个int型字段用来指代一些小型数据,例如Message的类型,一些消息Tag等等。

Object obj;

这个Object类型的字段用来放一些更加大型的数据,但是在单进程内部不推荐使用。

Bundle data;

这个Bundle类型的字段是用来传输数据的主要载体,Bundle类型作为Android数据传输的基本形式,以HashMap的方式存储多种类型的数据。

另外,基于前文提到的回调机制,在Message类的内部,同样提供了两个字段来替收到邮件的员工自动进行操作:

Handler target;
Runnable callback;

其中的target是一个Handler类的实例,他承担着邮件收发和处理小助手的角色,可以来自动的运行邮件回调里的操作(详见下文);而callback字段是一个Runable实例,相信熟悉JAVA线程的同学们对于它应该是非常了解了。通过这两者的配合,我们的邮件系统就可以在员工收到邮件之后自动进行处理,来弥补我们计算机这类特殊员工智商上的不足了。

题外话:由于我们的Message类在实例化一个对象的时候通常成本都是比较高的,所以我们的Android设计者们也对此做了一定的优化,采用一种Message池的思想,推荐我们Message的使用者通过Message类的静态方法obtainMessage()系列来获取Message实例,可以达到更好的应用性能。

邮件小助手——Handler

由于我们的员工是生活无法自理的计算机,所以要实现他们之间的良好通信,就必须要创建一个可以帮助员工收发和处理邮件的小助手,Handler类就此应运而生。Handler类的所有方法,我们整体来看的话,可以分为两大派系,发送和接收并处理,发送方面又分为三大块,获取Message,发送callback,发送DIY的Message:

//obtainMessage系方法来利用前文提到的优化方式获取Message对象,依参数的不同分别为Message初始化不同的数据或者操作部分
public final Message obtainMessage;
public final Message obtainMessage(int what);
public final Message obtainMessage(int what, Object obj);
public final Message obtainMessage(int what, int arg1, int arg2);
public final Message obtainMessage(int what, int arg1, int arg2, Object obj);
//post系方法用来接受用户提供的callback并由Handler封装成Message发送
public final boolean post(Runnable r);
public final boolean postAtTime(Runnable r, long uptimeMillis);
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis);
public final boolean postDelayed(Runnable r, long delayMillis);
public final boolean postAtFrontOfQueue(Runnable r);
//send系方法接受用户自定义的Message并直接发送(其中emptyMessage只接受what字段)
public final boolean sendMessage(Message msg);
public final boolean sendEmptyMessage(int what);
public final boolean sendEmptyMessageDelayed(int what, long delayMillis);
public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis);
public final boolean sendMessageDelayed(Message msg, long delayMillis);
public boolean sendMessageAtTime(Message msg, long uptimeMillis);
public final boolean sendMessageAtFrontOfQueue(Message msg);

题外话:从上文我们也可以看出,Handler在发送消息上还有一些高阶功能可以让我们来使用,那就是邮件调度功能,在上文的post系和send系方法中,均包含一些Delayed方法和AtTime方法,这两类方法分别表示在当前时间之上加延迟之后发送和在特定的时间发送,另外,在后文的邮件中转站中,我们会提到对邮件进行中转分发的基本数据结构,一个阻塞优先级队列(可以简单理解为可以插队的排队系统),我们的邮件助手Handler同样支持直接把Message插入队首最先处理,不过这个方法需要谨慎使用,滥用可能会导致原本在排队的消息一直得不到处理。

在发送之外的另一大派系,即是邮件的获取并处理,这一块Handler提供的方法非常简单,就只有两个方法:

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

public void handleMessage(Message msg) {
}

我们可以看到,在dispatchMessage方法中,Handler会首先去找Message中自带的callback内容进行执行,如果找不到的话,Handler自己也提供了一个callback字段,用户也可以给这个Handler赋相应的值来处理消息,如果这个字段也为空的话,最后Handler会调用handlerMessage方法,而我们看到,这个方法默认是个空方法,也就是说,用户在实例化自己的Handler对象并进行DIY定制的时候,一定要复写这个方法作为处理消息的备用方案,来保证消息得到处理

这样,通过这两大派系的方法,我们的Handler就可以很好的完成邮件的收发和处理工作了。但是在实际使用中,我们还需要一个邮箱一样的中转站,帮我们把收到的邮件都存储下来,然后一封封的交给我们的Handler来进行处理。这就引出了我们的Looper和MessageQueue的组合。

邮件中转站——Looper+MessageQueue

前文提到,MessageQueue通过阻塞优先级队列的方式,将用户收到的Message存储下来,供Handler依次处理,这里我们可以看出三个关键词:阻塞,优先级,队列,队列意味着所有Message会依照FIFO的方式被进行处理,优先级意味着该队列支持插队,针对优先级队列,一个很重要的因素就是,如何评判Message的优先级,在这里,我们按照Message需要被执行的时间作为评判Message优先级的指标。具体来讲,我们的Message类内部还维护有另一个字段

int when

这个字段表征我们的Message在收到之后多久应该被处理,而在Handler篇我们有提到Message调度的问题,利用post系和send系方法,我们可以发送DelayMessage或者AtTimeMessage,通过这些方法,我们直接或者间接的设定了Message的when字段,在MessageQueue内部,when的值越小,优先级越高,当when值为非正数时,会直接被插到队首优先执行。

阻塞,意味着我们的Message可能会存在同步关系,当我们一些Message的执行需要依赖其他事件的发生时,我们可以在队列中插入一个同步开关SyncBarrier,来阻塞所有同步Message,需要注意的是,该阻塞对于所有的异步消息(AsyncMessage)没有影响。

MessageQueue完成了对于一个邮件中转功能的底层封装,但是由于内部涉及大量的底层操作,对于用户并不友好,直接使用可能会出现很多问题,所以Android平台又提供了额外的一层封装,即我们的Looper(就好比在裸SMTP邮件服务器之外,一般都会有一层带有GUI的客户端才会给用户使用)。Looper对用户完全屏蔽了底层MessageQueue的实现细节,并完成了“邮箱登录注册”功能(线程绑定,详见下文),自动邮件中转,插入删除同步等等一系列操作。

作为邮件中转的核心功能,Looper类提供了一个静态方法loop,用来分发内部MessageQueue上所有的Message,其采用的基本思路是循环读取MessageQueue里的数据,当读到合法的Message对象时(不是SyncBarrier时),就调用对应的Message内部的target,通过回调机制进行自动处理(详见Message篇):

msg.target.dispatchMessage(msg);

而当遇到的是SyncBarrier且内部的MessageQueue里没有其他AsyncMessage或者此时MessageQueue已空时,Looper就会自动阻塞且让出系统资源,从而保证了整体的效率。

为了实现插入删除同步,Looper同样提供了一对方法:

public int postSyncBarrier();
public void removeSyncBarrier(int token);

在用户插入SyncBarrier的时候,方法会返回该Barrier的标识符token,在后面删除的时候,只要传入该token就可完成对该SyncBarrier的删除。

这样,我们的邮件系统功能上已经基本完备了,有邮件的中转,分发,处理,可以实现数据和操作的传输。但是还有一个非常重要的问题,现实中我们使用邮箱之前是一定要登录的,在这里同样如此。

线程身份证——ThreadLocal

从现实中,我们可以很轻易的看出,我们的邮箱和我们的邮件小助手,都是和我们自己绑定的,只有我们在登录确认身份之后,我们才能拿到属于我们自己的合法数据(自己的邮件)来进行进一步的处理。这里的情形同样如此。为了使这套机制正常运作,我们的Handler和Looper都必须和Thread进行绑定,才能保证数据和操作的正确处理。

Handler的线程绑定比较简单,Handler内部维护了两个字段

final MessageQueue mQueue;
final Looper mLooper;

分别映射创建该Handler对象的线程所拥有的Looper和该Looper内部的MessageQueue,即Handler通过绑定Looper,间接的绑定了Thread,另外,在上文Handler篇提到的所有发送Message的方法,在内部都会将Message的target字段改为当前Handler,以保证该Handler发送的Message一定会被该Handler自己处理(虽然笔者也不懂这里为啥要这么整)。

Looper对线程的绑定,全部借由其提供的prepare方法来实现:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

我们可以看到,Looper在内部维护有一个ThreadLocal字段,在prepare中,会首先利用ThreadLocal变量检查当前线程是否有绑定Looper,如果未绑定的话,实例化新的Looper对象并与当前线程进行绑定,同时,Looper的构造方法均为private,实例化唯一的入口就在prepare方法中,从而保证了每个线程只绑定一个Looper。

相信一些同学看到这里肯定会疑惑,这段代码里连Thread都没出现,绑定当前线程是怎么整的。答案就在最后一篇的主角——ThreadLocal里,在Looper的prepare方法里用到了ThreadLocal的两个方法,get和set:

public T get() {
    // Optimized for the fast path.
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values != null) {
        Object[] table = values.table;
        int index = hash & values.mask;
        if (this.reference == table[index]) {
           return (T) table[index + 1];
        }
     } else {
        values = initializeValues(currentThread);
     }

     return (T) values.getAfterMiss(this);
}

public void set(T value) {
   Thread currentThread = Thread.currentThread();
   Values values = values(currentThread);
   if (values == null) {
       values = initializeValues(currentThread);
   }
   values.put(this, value);
}

在这两个方法中,我们可以轻易找到当前线程的身影,在每个Thread对象中,会维护一个Values字段,这个类是ThreadLocal的一个内部类,他通过HashMap的方式,维护了一组<ThreadLocal, Object>的列表,ThreadLocal的get和set方法均会首先拿到当前线程的Values对象,然后对其进行操作(get方法的查找过程涉及到HashMap的具体实现,此处略过不表,有兴趣的同学可以自行查找ThreadLocal.java进行查看),看到这里,相信大家应该明白了,为什么此处ThreadLocal会成为我们这个机制里的线程身份证,Looper通过存储自己的static ThreadLocal对象,为自己准备了一组key和调用prepare方法的Thread进行绑定,后面该线程的所有通信操作,就将由绑定了该线程的Looper对象和绑定了该Looper对象的Handler对象共同来完成。

结尾

最后,我们再完整的梳理一遍Android线程通信机制的工作流程:

Thread对象在Runnable内部调用Looper.prepare(), 绑定Looper,完成“邮箱登录”
Thread对象创建Handler对象,自动绑定线程,复写handleMessage()方法,准备处理Message
Thread对象调用Looper.loop()方法,开始分发处理收到的Message
Thread对象获取其他Thread对象Handler,并发送Message
Thread对象被回收,或者主动调用Looper.quit(),“退出邮箱”

题外话:Android平台提供了HandlerThread供用户使用,可以免去用户自己手动Looper.prepare() 和Looper.loop()的麻烦

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android开发中,主线程(也被称为UI线程)负责处理与用户交互的操作,例如响应用户的点击事件、更新UI界面等。而子线程则是用于执行一些耗时的任务,比如网络请求、数据库操作等。 为了实现在子线程中更新UI界面的需求,Android提供了Handler和Looper机制。Looper是一个负责循环消息队列的类,它负责从消息队列中取出消息,然后将其分发给对应的Handler进行处理。 在子线程中使用Looper来处理消息时,需要首先调用Looper.prepare()方法来准备Looper,然后调用Looper.loop()方法来循环处理消息,直到Looper.quit()被调用停止循环。在调用Looper.loop()之前,需要先创建一个Handler的实例,并将其与当前线程的Looper关联起来。这样,子线程中的Looper才能将消息分发给相应的Handler进行处理。可以使用Handler的post()、sendMessage()等方法来向子线程的消息队列中发送消息。 使用子线程的LooperHandler机制,可以实现在子线程中更新UI界面的需求,避免在主线程中执行耗时操作导致界面卡顿的问题。然而,需要注意的是,在子线程中使用LooperHandler时要避免在UI界面的更新操作过于频繁,以免影响用户体验和性能。 总结一下,Android在子线程中使用LooperHandler的目的是为了实现在子线程中更新UI界面的需求,通过创建Looper实例并循环处理消息,将消息分发给对应的Handler进行处理。这样就能在子线程中更新UI界面,提高程序的性能和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值