Android全面解析之由浅及深Handler消息机制

本文全面系统地讲解了Android中的Handler消息机制,从入门到深入,涵盖Handler的定义、作用、使用方法以及内部模式结构。文章详细剖析了Handler、Message、MessageQueue和Looper四大关键类,解释了ThreadLocal在Handler机制中的作用,以及如何处理并发问题。此外,还探讨了HandlerThread的使用场景。通过对源码的分析,帮助读者深入理解Handler的运行流程和相关问题,以提升Android开发技能。
摘要由CSDN通过智能技术生成

文章已授权『郭霖』公众号发布

前言

很高兴遇见你~ 欢迎阅读我的文章。

关于Handler的博客可谓是俯拾皆是,而这也是一个老生常谈的话题,可见的他非常基础,也非常重要。但很多的博客,却很少有从入门开始介绍,这在我一开始学习的时候就直接给我讲Looper讲阻塞,非常难以理解。同时,也很少有系统地讲解关于Handler的一切,知识比较零散。我希望写一篇从入门到深入,系统地全面地讲解Handler的文章,帮助大家认识Handler。

这篇文章的讲解深度循序渐进,不同程序的读者可选择对应的部分查看:

  1. 第一部分是对于Handler的入门概述。了解一个新事物,需要问三个问题:是什么、为什么、怎么用。包括关于Handler的结构等都有介绍。
  2. 第二部分是在对Handler有一定的认知基础上,对各个类进行详细的讲解和源码分析。
  3. 第三部分是整体的流程分析以及常见问题的解析。
  4. 最后一部分是Android对于消息机制设计的讲解以及全文总结。

文章基本涵盖了关于Handler相关的知识,因而篇幅也比较长
考虑过把文章分割成几篇小文章,考虑到阅读的整体性以及方便性,最终还是集成了一篇大文章
文章成体系,全面地讲解知识点,而不是把知识碎片化,否则很难真正去理解单一的知识,更不易于对整体知识的把握
读者可自行选择感兴趣的章节阅读

那么,我们开始吧。

概述

什么是Handler?

准确来说,是Handler机制,Handler只是Handler机制中的一个角色。只是我们对Handler接触比较多,所以经常以Handler来代称。

Handler机制是Android中基于单线消息队列模式的一套线程消息机制。

他的本质是消息机制,负责消息的分发以及处理。这样讲可能有点抽象,不太容易理解。什么是“单线消息队列模式”?什么是“消息”?

通俗点来说,每个线程都有一个“流水线”,我们可往这条流水线上放“消息”,流水线的末端有工作人员会去处理这些消息。因为流水线是单线的,所有消息都必须按照先来后到的形式依次处理(在Handler机制中有“加急线”:同步屏障,这个后面讲)。如下图:

0073QO.png

放什么消息以及怎么处理消息,是需要我们去自定义的。Handler机制相当于提供了这样的一套模式,我们只需要“放消息到流水线上”,“编写这些消息的处理逻辑”就可以了,流水线会源源不断把消息运送到末端处理。最后注意重点:每个线程只有一个“流水线”,他的基本范围是线程,负责线程内的通信以及线程间的通信。每个线程可以看成一个厂房,每个厂房只有一个生产线。

两个关键问题

了解Handler的作用前需要了解Handler背景下的两个关键问题:

  1. 不能在非UI创建线程去操作UI
  2. 不能在主线程执行耗时任务

我们普遍的认知是:不能在非主线程更新UI。但这是不准确的,如果我们在子线程更新了UI,看看报错信息是什么:

笔者留下了英语渣渣的眼泪,百度翻译一下:

只有创建视图层次结构的原始线程才能访问其视图。但为什么我们一直都说是非主线程不能更新ui?这是因为我们的界面一般都是由主线程进行绘制的,所以界面的更新也就一般都限制在主线程内。这个异常是在viewRootIimpl.checkThread()方法中抛出来的,那可不可以绕过他?当然可以,在他还没创建出来的时候就可以偷偷更新ui了。阅读过Activity启动流程的读者知道,ViewRootImpl是在onCreate方法之后被创建的,所以我们可以在onCreate方法中创建个子线程偷偷更新UI。(Actvity启动流程解析传送门)但还是那句话,可以,但没必要去绕过这个限制,因为这是谷歌为了我们的程序更加安全而设计的。

为什么不能在子线程去更新UI?因为这会让界面产生不可预期的结果。例如主线程在绘制一个按钮,绘制一半另一个线程突然过来把按钮的大小改成两倍大,这个时候再回去主线程继续执行绘制逻辑,这个绘制的效果就会出现问题。所以UI的访问是决不能是并发的。但,子线程又想更新UI,怎么办?加锁。加锁确实可以解决这个问题,但是会带来另外的问题:界面卡顿。锁对于性能是有消耗的,是比较重量级的操作,而ui操作讲究快准狠,加锁会让ui操作性能大打折扣。那有什么更好的方法?Handler就是解决这个问题的。

第二个问题,不能在主线程执行耗时操作。耗时操作包括网络请求、数据库操作等等,这些操作会导致ANR(Application Not Responding)。这个是比较好理解的,没有什么问题,但是这两个问题结合起来,就有大问题了。数据请求一般是耗时操作,必须在子线程进行请求,而当请求完成之后又必须更新UI,UI又只能在主线程更新,这就导致必须切换线程执行代码,上面讨论了加锁是不可取的,那么Handler的重要性就体现出来了。

不用Handler可不可以?可以,但没必要。Handler是谷歌设计来方便开发者切换线程以及处理消息,然后你说我偏不用,我自己用Java工具类,自己弄个出来不可以吗?那。。。请收下小的膝盖。

为什么要有Handler?

先给结论:

  1. 切换代码执行的线程
  2. 按顺序规则地处理消息,避免并发
  3. 阻塞线程,避免让线程结束
  4. 延迟处理消息

第一个作用是最明显也是最常用的,上一部分已经讲了Handler存在的必要性,android限制了不能在非UI创建线程去操作UI,同时不能在主线程执行耗时任务,所以我们一般是在子线程执行网络请求等耗时操作请求数据,然后再切换到主线程来更新UI。这个时候就必须用到Handler来切换线程了。上面讨论过了这里不再赘述。

这里有一个误区是:我们的activity是执行在主线程的,我们在网络请求完成之后回调主线程的方法不就切换到主线程了吗?咳咳,不要笑,不要觉得这种低级错误太离谱,很多童鞋刚开始接触开发的时候都会犯这个思维错误。这其实是理解错了线程这个概念。代码本身并没有限制运行在哪个线程,代码执行的线程环境取决于你的执行逻辑是在哪个线程。这样讲可能还是有点抽象。例如现在有一个方法void test(){},然后两个不同的线程去调用它:

new Thread(){
   
    // 第一个线程调用
    test();
}.start();

new Thread(){
   
    // 第二个线程调用
    test();
}

此时虽然都是test这个方法,但是他的执行逻辑是由不同的线程调用的,所以他是执行在两个不同的线程环境下。而当我们想要把逻辑切换到另一个线程去执行的时候,就需要用到Handler来切换逻辑。

第二个作用可能看着有点懵。但其实他解决了另一个问题:并发操作。虽然切换线程解决了,如果主线程正在绘制一个按钮,刚测量好按钮的长宽,突然子线程一个新的请求过来打断了,先停下这边的绘制操作,把按钮改成了两倍大,然后逻辑切回来继续绘制,这个时候之前的测量的长宽已经是不准确的了,绘制的结果肯定也不准确。怎么解决?单线消息队列模型。在讲什么是Handler那部分简单介绍过,就是相当于一个流水线一样的模型。子线程的请求会变成一个个的消息,然后主线程依次处理,那么就不会出现绘制一半被打断的问题了。

同时这种模型也不止用于解决ui并发问题,在ActivityThread中有一个H类,他其实就是个Handler。在ActivityThread中定义了一百多中消息类型以及对应的处理逻辑,这样,当需要让ActivityThread处理某一个逻辑的时候,只需要发送对应的消息给他即可,而且可以保证消息按顺序执行,例如先调用onCreate再调用onResume。而如果没有Hanlder的话,就需要让ActivityThread有一百多个接口对外开放,同时还需要不断进行回调保证任务按顺序执行。这显然复杂了非常多。

我们执行一个Java程序的时候,从main方法入口,执行完成之后,马上就退出了,但是我们android应用程序肯定是不可以的,他需要一直等待用户的操作。而Handler机制就解决了这个问题,但消息队列中没有任务的时候,他就会把线程阻塞,等到有新的任务的时候,再重新启动处理消息。

第四个作用让延迟处理消息得到了最佳解决方案。假如你想让应用启动5秒后界面弹出一个对话框,没有handler的情况下,会如何处理?开一个Thread然后使用Thread.sleep让线程睡眠一对应的时间对吧,但如果多个延迟任务呢?而开启线程也是个比较重量级的操作且线程的数量有限。而可以直接给Handler发送延迟对应时间的消息,他会在对应时间之后准时处理该消息(当然有特殊情况,如单件消息处理时间过长或者同步屏障,后面会讲到)。而且无论发送多少延迟消息都不会对性能有任何影响。同时,也是通过这个功能来记录ANR的时间。

讲这些作用可能读者心中并没有一个很形象的概念,也可能看完就忘了。但是关于Handler的定义不能忘:Handler机制是Android中基于单线消息队列模式的一套线程消息机制。,上述四个作用是为了让读者更好地理解Handler机制。

如何使用Handler

我们平常使用Handler有两种不同的创建方式,但总体流程是相同的:

  1. 创建Looper
  2. 使用Looper创建Handler
  3. 启动Looper
  4. 使用Handler发送信息

Looper可理解为循环器,就像“流水线”上的滚带,后面会详细讲到。每个线程只有一个Looper,通常主线程已经创建好了,追溯应用程序启动流程可以知道启动过程中调用了Looper.prepareMainLooper,而在子线程就必须使用如下方法来初始化Looper:

Looper.prepare();

第二步是创建Handler,也是最熟悉的一步。我们有两种方法来创建Handler:传入callBack对象和继承。如下:

public class MainActivity extends AppComposeActivity{
   
    ...;
    // 第一种方法:使用callBack创建handler
    public void onCreate(Bundle savedInstanceState){
   
        super.onCreate(savedInstanceState);
        Handler handler = Handler(Looper.myLooper(),new CallBack(){
   
            public Boolean handleMessage(Message msg) {
   
                TODO("Not yet implemented")
            }
        });
    }
    
    // 第二种方法:继承Handler并重写handlerMessage方法
    static MyHandler extends Hanlder{
   
        public MyHandler(Looper looper){
   
            super(looper);
        }
        @Override
        public void handleMessage(Message msg){
   
            super.handleMessage(msg);
            // TODO(重写这个方法)
        }
    }
}

注意第二种方法,要使用静态内部类,不然可能会造成内存泄露。原因是非静态内部类会持有外部类的引用,而Handler发出的Message会持有Handler的引用。如果这个Message是个延迟的消息,此时activity被退出了,但Message依然在“流水线”上,Message->handler->activity,那么activity就无法被回收,导致内存泄露。

两种Handler的写法各有千秋,继承法可以写比较复杂的逻辑,callback法适合比价简单的逻辑,看具体的业务来选择。

然后再调用Looper的loope方法来启动Looper:

Looper.loop();

最后就是使用Handler来发送信息了。当我们获得handler的实例之后,就可以通过他的sendMessage相方法和post相关方法来发送信息,如下:

handler.sendMessage(msg);
handler.sendMessageDelayed(msg,delayTime);
handler.post(runnable);
handler.postDelayed(runnable,delayTime);

然后一般情况下是哪个Handler发出的信息,最终由哪个Handler来处理。这样,只要我们拿到Handler对象,就可以往对应的线程发送信息了。

Handler内部模式结构

经过前面的介绍对于Looper已经有了一定的认知,但可能对他内部的模式还不太清楚。这一部分先讲解Handler的大概内部模式,目的是为下面的详解做铺垫,为做整体概念感知。先上图:

Handler机制内部有三大关键角色:Handler,Looper,MessageQueue。其中MessageQueue是Looper内部的一个对象,MessageQueue和Looper每个线程有且只有一个,而Handler是可以有很多个的。他们的工作流程是:

  1. 用户使用线程的Looper构建Handler之后,通过Handler的send和post方法发送消息
  2. 消息会加入到MessageQueue中,等待Looper获取处理
  3. Looper会不断地从MessageQueue中获取Message然后交付给对应的Handler处理

这就是大名鼎鼎的Handler机制内部模式了,说难,其实也是很简单。

Handler机制关键类

一、ThreadLocal

概述

ThreadLocal是Java中一个用于线程内部存储数据的工具类。

ThreadLocal是用来存储数据的,但是每个线程只能访问到各自线程的数据。我们一般的用法是:

ThreadLocal<String> stringLocal = new ThreadLocal<>();
stringLocal.set("java");
String s = stringLocal.get();

不同的线程之间访问到的数据是不一样的:

public static void main(String[] args){
   
    ThreadLocal<String> stringLocal = new ThreadLocal<>();
	stringLocal.set("java");
    
    System.out.println(stringLocal.get());
    new Thread(){
   
        System.out.println(stringLocal.get());
    }
}

结果:
java
null

线程只能访问到自己线程存储的数据。

ThreadLocal的作用

ThreadLocal的特性适用于同样的数据类型,不同的线程有不同的备份情况,如我们这篇文章一直在讲的Looper。每个线程都有一个对象,但是不同线程的Looper是不一样的,这个时候就特别适合使用ThreadLocal来存储数据,这也是为什么这里要讲ThreadLocal的原因

ThreadLocal内部结构

ThreadLocal的内部机制结构如下:

每个Thread,也就是每个线程内部维护有一个ThreadLocalMap,ThreadLocalMap内部存储多个Entry。Entry可以理解为键值对,他的本质是一个弱引用,内部有一个object类型的内部变量,如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
   
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
   
        super(k);
        value = v;
    }
}

Entry是ThreadLocalMap的一个静态内部类,这样每个Entry里面就维护了一个ThreadLocal和ThreadLocal泛型对象。每个线程的内部维护有一个Entry数组,并通过hash算法使得读取数据的速度达到O(1)。由于不同的线程对应的Thread对象不同,所以对应的ThreadLocalMap肯定也不同,这样只有获取到Thread对象才能获取到其内部的数据,数据就被隔离在不同的线程内部了。

ThreadLocal工作流程

那ThreadLocal是怎么实现把数据存储在不同线程中的?先从他的set方法入手:

TheadLocal.class
    
public void set(T value) {
   
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

逻辑不是很复杂,首先获取当前线程的Thread对象,然后再获取Thread的ThreadLocalMap对象,如果该map对象不存在则创建一个并调用他的set方法把数据存储起来。我们继续看ThreadLocalMap的set方法:

ThreadLocalMap.class

private void set(ThreadLocal<?> key, Object value) {
   
    // 每个ThreadLocalMap内部都有一个Entry数组
    Entry[] tab = table;
    int len = tab.length;
    // 获取新的ThreadLocal在Entry数组中的下标
    int i = key.threadLocalHashCode & (len-1);
    // 判断当前位置是否发生了Hash冲突
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
   
        ThreadLocal<?> k = e.get();

        // 如果数据存在且相同则直接返回
        if (k == key) {
   
            e.value = value;
            return;
        }
        if (k == null) {
   
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 若当前位置没有其他元素则直接把新的Entry对象放入
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 判断是否需要对数组进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

这里的逻辑和HashMap是很像的,我们可以直接使用HashMap的思维来理解ThreadLocalMap:ThreadLocalMap的key是ThreadLocal,value是ThreadLocal对应的泛型。他的存储步骤如下:

  1. 根据自身的threadLocalHashCode与数组的长度进行相与得到下标
  2. 如果此下标为空,则直接插入
  3. 如果此下标已经有元素,则判断两者的ThreadLocal是否相同,相同则更新value后返回,否则找下一个下标
  4. 直到找到合适的位置把entry对象插入
  5. 最后判断是否需要对entry数组进行扩容

是不是和HashMap非常像?和HashMap的不同是:hash算法不一样,以及这里使用的是开发地址法,而HashMap使用的是链表法。ThreadLocalMap牺牲一定的空间来换取更快的速度。具体的Hash算法这里就不再深入了,有兴趣的读者可以阅读这篇文章ThreadLocal传送门

然后继续看ThreadLocal的get方法:

ThreadLocal.class

public T get() {
   
    // 获取当前线程的ThreadLocalMap
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
   
        // 根据ThreadLocal获取Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果没找到也会执行初始化工作
        if (e != null) {
   
            @SuppressWarnings("unchecked")
            // 把获取到的对象进行返回
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

前面讲到ThreadLocalMap其实非常像一个HashMap,他的get方法也是一样的。使用ThreadLocal作为key获取到对应的Entry,再把value返回即可。如果map尚未初始化则会执行初始化操作。下面继续看下ThreadLocalMap的get方法:

ThreadLocalMap.class

private Entry getEntry(ThreadLocal<?> key) {
   
    // 根据hash算法找到下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 找到数据则返回,否则通过开发地址法寻找下一个下标
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

利用ThreadLocal的threadLocalHashCode得到下标,然后根据下标找到数据。没找到则根据算法寻找下个下标。

内存泄露问题

我们会发现Entry中,ThreadLocal是一个弱引用,而value则是强引用。如果外部没有对ThreadLocal的任何引用,那么ThreadLocal就会被回收,此时其对应的value也就变得没有意义了,但是却无法被回收,这就造成了内存泄露。怎么解决?在ThreadLocal回收的时候记得调用其remove方法把entry移除,防止内存泄露。

ThreadLocal总结

ThreadLocal适合用于在不同线程作用域的数据备份

ThreadLocal机制通过在每个线程维护一个ThreadLocalMap,其key为ThreadLocal,value为ThreadLocal对应的泛型对象,这样每个ThreadLocal就可以作为key将不同的value存储在不同Thread的Map中,当获取数据的时候,同个ThreadLocal就可以在不同线程的Map中得到不同的数据,如下图:

ThreadLocalMap类似于一个改版的HashMap,内部也是使用数组和Hash算法来存储数据,使得存储和读取的速度非常快。

同时使用ThreadLocal需要注意内存泄露问题,当ThreadLocal不再使用的时候,需要通过remove方法把value移除。

二、Message

概述

Message是负责承载消息的类,主要是关注他的内部属性:

// 用户自定义,主要用于辨别Message的类型
public int what;
// 用于存储一些整型数据
public int arg1;
public int arg2;
// 可放入一个可序列化对象
public Object obj;
// Bundle数据
Bundle data;
// Message处理的时间。相对于1970.1.1而言的时间
// 对用户不可见
public long when;
// 处理这个Message的Handler
// 对用户不可见
Handler target;
// 当我们使用Handler的post方法时候就是把runnable对象封装成Message
// 对用户不可见
Runnable callback;
// MessageQueue是一个链表,next表示下一个
// 对用户不可见
Message next;
循环利用Message

当我们获取Message的时候,官方建议是通过Message.obtain()方法来获取,当使用完之后使用recycle()方法来回收循环利用。而不是直接new一个新的对象:

public static Message obtain() {
   
    synchronized (sPoolSync) {
   
        if (sPool != null) 
  • 44
    点赞
  • 99
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值