Android消息机制详解:Handler、MessageQueue、Looper

1、Handler的诞生背景

在Android中,规定了只能在主线程(或者叫UI线程)中,去进行UI相关的操作,而其他线程则无法操作UI,否则报错;
但同时,由于不能再UI线程进行耗时的操作,否则会报ANR异常, 因此,我们通常又把耗时操作放到子线程去进行一个处理。
那这就涉及到了一个数据传递的问题,我们在子线程处理的数据,怎么传递到主线程呢?这就有了Handler消息机制。

为什么要设计为只能在主线程进行UI操作而其他子线程则不行呢?

  • 1、多线程访问UI会导致结果不可控,这是典型的多线程问题,而如果加锁,由于UI那么多,可能逻辑也会变得复杂,甚至出现死锁;
  • 2、即使加锁,也会放UI的访问效率降低,而对于用户直接交互的UI,从人机交互的角度来讲,要尽可能高效;

因此,最简单高效的方式就是只让一个线程来处理UI操作,其他线程处理完后,通过handler通知主线程进行更新即可。这是一种典型的生产者-消费者模式, 其他线程生产消息,并把消息放入到一个队列中,而主线程来这个队列取出消息消费。

2、Handler消息机制的结构

Android消息机制主要由三部分组成:

  • 1、Handler:负责发送消息, 以及处理回调结果;
  • 2、MessageQueue:负责存储消息;
  • 3、Looper:负责处理消息,处理完后回调给Handler;

需要注意的是:

  • 1、如果使用的是handler.post(Runnable)方法,则looper处理消息时,调用的是Runnable的run方法;
  • 2、如果使用的是handler.sendMessage方法, 则回调给Handler的handleMessage方法,此时,一般需要自己重写这个handleMessage方法;
  • 3、如果创建Handler对象时使用的Callback接口, 则回调给Callback接口的handleMessage方法;

3、为什么要在主线程中创建Handler对象?

我们通常在主线程中创建Handler对象后,然后在子线程中处理完耗时操作(IO/网络等),然后再通过handler发送消息。为什么要强调在主线程中创建Handler对象呢? 如果不在主线程创建Handler,就会报一下错误:

java.lang.RuntimeException: Can't create handler inside thread Thread that has not called Looper.prepare()

这是Android开发一个很常见的错误,如果想要在非主线程使用handler,就必须在线程的run方法的第一行调用Looper.preper()方法, 在run方法的最后一行调用Looper.loop()方法, 才行, 不过这个子线程的Handler不能处理UI相关的消息。

所以我们通常在Activity中或者Service的代码块中创建Handler对象, 或者在创建Handler时,传入主线程的Looper对象,如下:

Handler handler = new Handler(Looper.getMainLooper());

因为只有这样,我们创建的Handler对象才能把消息发送到主线程的MessageQueue中,才能被主线程中的Looper取出进行处理。

具体源码,在下面第5小节源码分析。

4、为什么在主线程的Handler中可以处理UI操作?

因为handler发送的消息是在被主线程中被Looper从MessageQueue中取出来进行处理的,所以这一段代码自然是运行在主线程中的。

具体源码分析见第5小节。

5、源码分析

我们老是说主线程,主线程,到底什么是主线程呢?其实Android中的主线程,就是ActivityThread,而我们一个APP进程的入口函数,就是ActivityThread.main(), 主要代码如下:

public static void main(String[] args) {
	 Looper.prepareMainLooper();
	...
	 
    Looper.loop();	
	throw new RuntimeException("Main thread loop unexpectedly exited");
}

可以看到,在main方法的开始位置,就调用了 Looper.prepareMainLooper(), 完成了对主线程Looper对象的初始化,然后在结束位置调用了 Looper.loop(), 这个方法就是一个死循环,不断去MessageQueue中读取消息。因此,执行了Looper.loop()后,在此方法之后的代码理论上就不会被执行,因此在最后抛出一个RuntimeException, 因为如果执行到这一句,那肯定是程序出错了。

我们看一下这个loop方法主要做了些什么:

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;

    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {
        Message msg = queue.next(); //注释1、去MessageQueue中获取消息,如果没有消息,则阻塞在这里

        final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

        final long traceTag = me.mTraceTag;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        final long end;
        try {
            msg.target.dispatchMessage(msg);//注释2、此处回调给Handler的handleMessage方法或传入的Runnable接口进行处理
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        msg.recycleUnchecked();
    }
}

上述代码中的注释1,是从MessageQueue中去取出一个消息,如果暂时没有消息,则阻塞在这里,直到来了新消息。

注释2 回调Handler对象的handleMessage方法进行代码处理,如果创建Handler对象时没有重写handleMessage,而是传入的Runnable或Callback接口,则调用这两个接口的方法。

所有的这一段代码都是执行在ActivityThread.main()方法中的,所以是运行在主线程中的。

6、ThreadLocal类

前面我们将Handler消息机制的内容从整体上就讲完了,但是还有一些细节没有说,这里补充一个很重要的知识点:ThreadLocal类。这个类日常开发中用的不多,但是在某些场景下却可以帮助我们轻松实现一些看起来复杂的问题。在Android源码中其中一个使用场景就是Handler。

一般来说,当某个数据的作用域是在某个线程时,而不同线程可能有不同数据时,可以考虑使用ThreadLocal。类似于我们new了一个对象,这个类中某个字段在不同对象中,它的值可能不同,但是这个和ThreadLocal不同的是,类中的字段时我们写类的代码时就定义好的,但是Thread线程对象中一开始并没有我们想要的某个字段,因此,我们可以通过ThreadLocal来额外添加。

比如以下代码:

private static ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<>();

public static void main(String[] args) {
	mThreadLocal.set(true);//main线程设为true
	
	new Thread() {
		public void run() {
			mThreadLocal.set(false);
			System.out.println("Thread 111 mThreadLocal.get: "+ mThreadLocal.get());
		};
	}.start();
	
	new Thread() {
		public void run() {
			System.out.println("Thread 222 mThreadLocal.get: "+ mThreadLocal.get());
		};
	}.start();

	System.out.println("Thread main mThreadLocal.get: "+ mThreadLocal.get());
}

执行的结果为:

Thread 111 mThreadLocal.get: false
Thread main mThreadLocal.get: true
Thread 222 mThreadLocal.get: null

三个线程执行的先后顺序不同,但看结果还是比较明显的, 在main线程,我们设置为true,获取的值也是true,在111线程设置为false,获得的结果就是false, 而线程222没有设置任何值, 结果为null。

通过ThreadLocal,我们可以为APP进程设置一些作用域为线程的变量,简单方便,而如果不用ThreadLocal,通常的做法就是, 要么设置回调接口, 一层一层往下传递,要么设置静态接口, 或者设置静态变量来调用。 如果是接口,传递代码过深,跟踪起来会比较麻烦,而且每个方法形参都要传入一个接口,设计得比较复杂,而如果使用静态变量,那需要很多值呢,岂不是的设置很多不同的变量名称,代码看起来会比较冗余。

而使用ThreadLocal,直接在当前线程中调用set方法,然后在当前线程需要的地方调用get方法即可,简单高效。这个代码设计简直太优秀了!

在上述代码中,我们看到,要使用ThreadLocal,通常要指定一个泛型,然后调用set的时候,传入这个泛型即可,调用get即可得到我们设置的值。

以上就是关于Handler的知识点,如果您觉得内容还行,麻烦点个赞吧~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值