完全解析Android:Looper与Handler机制

4,656 阅读15分钟

Looper与Handler机制

该文章将从源码和应用的角度理解Looper和Handler机制,并给出一些基本的使用。

前置准备

对于疏漏或者不理解的地方,还是需要自行阅读源码。

导语

在开始之前我想先说一下我个人对如何学习的看法。学习过程中有一个非常重要的做法,就是把一些暂时没法完全理解的知识当成黑盒。所谓黑盒就是不求甚解,避免钻牛角尖,只理解它提供的功能,不用去理解底层实现。一些可能不需要掌握的知识先当成黑盒子看待,而不是一直钻一直钻,忘记了一开始学习的目的,“钻”到最后发现根本“钻”不完,丢下一句“毁灭吧我累了”。这些知识有些根本没必要掌握,有些知识没必要现在就掌握,有时间再去掌握就行了。

举个例子,要理解一个"Hello World"的程序是怎么运行的,我们并不需要去理解cpu设计、指令集、半导体、甚至是硅的开采与加工又或者说分子原子相互作用的理论。大概搞懂编译、链接、汇编的一个流程就够了(当然还是看你学习的目标)。

LooperHandler机制是Android程序运行的核心机制,该机制渗透在Android应用程序的方方面面,理解该机制之后我们才能理解Android开发中的许多其他的概念,我认为这是每个Android开发者都应该掌握的。

这里我们将大概从下面的思路来讲解该机制:为什么需要Looper与Handler机制?它是怎么运作的?我们在开发时怎么使用?(Why, What, How

为什么需要该机制?

因为单线程UI开发被证实为一种正确的模式,多线程UI开发繁琐、容易出错。

浏览器是单线程模型,windows界面开发框架wpf的UI只能在主线程更新,似乎所有的UI开发框架都是这样的一个原则:必须在主线程更新UI。所以这种机制最本质的目的是为了保证线程安全。

那多线程UI就一定不安全吗?一些参考资料上写着:“人们也尝试过很多多线程UI的方案,最终放弃了”。可以理解到,单线程UI确实是一种被人们的经验证实为正确的方式。其实也很好理解:假设我们在多个线程(或者具体点:多个网络请求的回调)里更新UI,最终我们一定要自己去加锁,去实现线程同步,不仅繁琐,还极大地提高出错的可能性,最终可能我们需要自行去实现一套这样的Looper/Handler机制,所以现在Android直接采用了这种机制。

现在把我们的视角放到Android系统被设计出来之前,如果让我们来设计一套单线程的UI、线程安全的机制,我们要怎么做?我们希望它满足:

  • 可以处理各种各样的“信号”。所谓“信号”,更具体点来讲,就是多线程之间的交互,别的线程(系统层、应用层)怎么控制该UI线程、与之通信;除了我们自身应用的需求外,系统信号:屏幕操作、鼠标、按键等等也是需要与我们的程序交互的。
  • 不需要我们去做繁琐的同步、加锁
  • 线程安全

其实很容易我们就会想到“生产-消费者”模式,因为它实在用得太广泛了,浏览器、消息队列、各种UI框架使用,而且它确实非常好用。在《Android并发开发》一书中该模式被描述为一种"安全发布的习惯用法"。

基本的生产-消费者模型:

  • 消息:“消息”其实会给人一种很轻量化、只是一个“标志”的感觉,但实际上“消息”可以是任何数据,例如:Runable接口,可以包含一些操作直接丢给消息队列,消费者消费时直接执行这些操作。

  • 生产者:生产“消息”,然后丢到队列里。

  • 消费者:其实有些人可能会混淆,什么是消费者?我的理解是数据的处理方,就是消费者。其实概念不重要。这里还有一个角色,循环,一般的做法是用一个管理类来循环获取数据,并让消费者进行消费,而不是让消费者自行去队列获取数据。非要给这个循环的管理类一个称谓的话,我们称之为Looper(循环器...?😨)吧。

    那要怎么“打破”这个循环呢?可以这样:发送一个退出的消息,消费者接收到这样的消息就退出循环。为了程序的健壮性,退出消费者的循环后需要停止整个“生产-消费”的过程,不能让生产者继续往已经不再消费的队列里生产数据。

  • 消息队列:顾名思义,就是在生产者和消费者之间传递消息的队列。由于生产者和消费者可能在多个不同的线程,所以该队列必须要保证线程安全,最简单的方式(也是最常用的)就是用一个阻塞队列。或者可以这么说,消息队列一定是阻塞队列,因为在没有消息和多个线程同步读写时,它就会阻塞。

一个更具体的生产-消费者模式

怎么用“生产-消费者”模式实现线程安全的UI呢?

  1. 我们在程序的入口初始化消息队列消费者循环,程序入口的线程我们称为主线程(也就是UI线程)。
  2. 限制所有的UI操作必须在主线程进行(其实就是不在主线程操作就报错就可以了= =)。
  3. 当我们需要对UI操作或者是任何主线程的操作时,通过将操作封装为消息(Message推到消息队列中,这时消费者循环就会对消息进行处理了。

可以看到,由于每个消息的处理都是出队列后在主线程操作,处理完才处理下一个操作,这样就保证了每个操作的原子性,当然也就满足线程安全了。

所以Looper/Handler其实并没有什么高大上的东西,它的核心机制我们平常开发都会用到。由于这是Android应用的基本并发机制,所以消息的生产和处理是非常频繁的,下面我们结合源码来讲解一下具体实现的细节。

如何运作的?

大概的流程如上图所示,具体的细节我们结合源代码进行分析。思路是这样的:

  • 消费者部分:我们先从程序的入口开始,看看Looper循环是如何初始化和开始循环的,它如何获得消息,如何对消息进行处理。
  • 生产者部分:我们以自己平时是如何生产消息为例子,然后分析消息是如何推入队列的。
  • 消息队列:MessageQueue,消息队列并不会按照顺序单独分为一个部分来讲解,因为它的逻辑是糅合在生产-消费的过程中的。所以只要把上面的部分理解了,消息队列的部分自然就理解了。这里我们可以先补充一些概念:
    • 消息队列是用链表实现的
    • 它包含了数据、对应的Handler、期待它被处理的时间

OK,这里还需要向读者说明的是,在理解时如果能带着“自顶向下”的思路来阅读,会帮助更好地掌握。所以带着上面那个图阅读下面的部分吧。

消费者-流程

在程序的入口开启消费者循环:Looper.loop(),我们从程序入口开始,自顶向下分析其调用层次和细节。

程序入口

在android源码->ActivityThread->main()里,这是一个android app的入口。

  • prepareMainLooper()

  • loop()

loop()里的细节

要理解loop()之前,我们先想一下一个Message在循环过程中需要包含什么:

  • 数据
  • 期待该消息被处理的时间

数据自不用多说,而有了时间之后我们才能有序地规划各种任务,所以时间也是非常重要的。

截取一部分结构来看一下:

when字段就是期待它被处理的时间。

其他的像:target是处理该消息的Handler对象,argobjwhat等都是一些携带的信息、pool是复用机制用到的、next当然就是链表结构用到的...细节不用太过纠结,可以把它们理解为数据即可。

所以目标就是:让我们循环地获取已经准备好了(时间到了)的消息,并对它进行处理。

  • quene.next()

    这里我们之间贴一些大概的源码,这个方法很重要所以后面会有细节的分析,大概做了下面几件事情:

    • 返回已经准备好了的下一条消息,如果没有就阻塞。[这是最重要的]

      当然了,除了最重要的部分,当然还做了一些其他的事,如下:

    • 退出机制

    • 希望它在当前没有可用消息时可以做一些额外的事情:比如GC回收

  • msg.target.diapatchMessage()

    处理消息

    Handler处理信息的优先级:

    • 1.callback:优先级最高,这是一个Runable,当我们调用post(Runable r)时其实就是用这个变量。

    • 2.mCallback:优先级次之,我们在初始化Handler时可以传进去:

      它是一个Callback接口,只有一个方法:handleMessage(@NonNull Message msg)

      注意:if (mCallback.handleMessage(msg)) { return; },所以当该接口返回true时则不希望Handler.handleMessage()再进行处理。

    • 3.handleMessage():优先级最低,我们继承该Handler时,通过重写该方法来处理消息。在Handler中默认不作任何处理:

  • msg.recycleUnchecked()

    Message的回收。之前我们说过Message有一个复用机制,其实就是把消息的各个变量重置一下,然后加到复用池(其实就是一个链表)的表头:

MessageQueue.next()的细节

  1. 退出机制

  2. nativePollOnce(long ptr, int timeoutMillis)

    这个调用会调用native方法,我们阻塞、唤醒的核心机制就是由该方法提供的。

    它的调用层次如下:

    可以看到,核心等待的函数是:epoll_wait()

    epoll机制的细节这里不会展开,可以看这篇文章:如果这篇文章说不清epoll的本质,那就过来掐死我吧! (1) - 知乎 (zhihu.com);但是它大概就是这样的用法:

    1.监听一些句柄->2.当这些句柄写入数据时,它就可以被唤醒。当然它还支持设置TimeOut,这也就是我们超时时间的底层机制。

    在服务器设计中,一个基本的用法:

    int s = socket(AF_INET, SOCK_STREAM, 0);   
    bind(s, ...)
    listen(s, ...)
    
    int epfd = epoll_create(...);
    epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
    
    while(1){
        int n = epoll_wait(...)
        for(接收到数据的socket){
            //处理
        }
    }
    

    核心是两个函数:epoll_ctlepoll_wait

    Looper()的构造函数,就会将待监听句柄(这里包括:mWakeEventFd)加入到epoll监听列表中

    很多时候需要唤醒,比如在jave层调用nativeWake时,就是向mWakeEventFd写入一个值:

  3. 当唤醒时,查找是否有可用的Message,有的话返回:

  4. 空闲时做一些额外的操作:idle handler

    这个操作在一次next()调用过程中只会进行一次。

    在第一次迭代时,获取mIdleHandlers的值,然后后面进行遍历,逐一调用.queueIdle()方法。

    IdleHandler接口是这样的:

    当我们需要一个这样的空闲处理Handler时,就实现这个接口,把要做的操作放在里面,然后添加到mIdleHandlers里就可以了(通过addIdleHandler(@NonNull IdleHandler handler)方法)。

生产者-流程

生产者部分要做的事情就是:产生消息->推到消息队列正确的位置

当然我们并不会直接从消息队列里推消息,而是通过Handler来完成的,一个简单的例子如下:

这里具体的调用层级我们就省略了,反正最终都是调用一个函数:MessageQueue.enqueueMessage(msg, uptimeMillis);,它做的事情很简单:将Message按时间顺序插入到合适的位置,并决定是否需要唤醒阻塞的等待(nativePollOnce

  • 插在表头:当前时间比其余所有消息的时间都小

    由于我们插入了更近时间的消息,所以如果当前阻塞,则需要唤醒。

  • 插在中间:按照时间顺序插在中间

    这里唤醒的判断是这样的:

    1. 如果我们的消息不是异步消息,又是插在消息队列的中间,这就说明该消息的执行时间一定比之前的消息更远,所以不需要唤醒。

    2. 如果我们的消息是异步消息,异步消息是什么意思呢?就是当存在屏障(targetnull)时,我们就不考虑同步的消息了:

      这个逻辑可以在Message.next()中找到。

      所以如果是异步消息,且存在屏障,那么我们即使是插在中间,也要考虑是否唤醒。什么时候唤醒呢?当我们插入的异步消息是插在异步消息的表头时,就需要唤醒。

使用

在理解了它的原理后,我们一般是如何来使用这个机制呢?

  • 在主线程进行UI更新

    可以在任何线程上拿到主循环的Looper:Looper.getMainLooper(),然后通过Handler(可以new一个,也可以放在一个全局变量,甚至可以放到一个Utils里)去发送消息:

    • sendMessage()
    • post()

    还可以指定延迟时间postDelayed()等等。

  • 在我们自己的线程里使用LooperHandler机制

    这样做有什么好处呢?好处就是你可以使用它们提供好的API,省去很多麻烦。当然自己写一套生产-消费者模式也并不麻烦(但是需要定时任务就比较麻烦了!)。

    比如说我们想写一个在工作线程去执行一些耗时任务,就可以在一个线程里初始化LooperHandler,然后之后我们需要做什么任务就通过Handler丢到队列里,Looper就会自动去取消息并执行了。比如这样:

    开始一个循环需要两步:

    1. 初始化LooperLooper.prepare()
    2. 开始循环:Looper.loop()
            new Thread(()->{
                Looper.prepare();
                mWorkLooper = Looper.myLooper();
                Looper.loop();
            }).start();
    

    我们写一个测试程序来看看:

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            new Thread(()->{
                Log.d("ZHUTAG", "WorkLooper里的线程:"+Thread.currentThread());
                Looper.prepare();
                mWorkLooper = Looper.myLooper();
                Looper.loop();
            }).start();
    
            Log.d("ZHUTAG", "主线程:"+Thread.currentThread());
            try {
                Thread.sleep(1000); // 这里是为了防止Looper还没初始化
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            workHandler = new Handler(mWorkLooper);
            workHandler.postDelayed(()->{
                Log.d("ZHUTAG", "收到消息!!当前线程:"+Thread.currentThread());
            }, 5000);
        }
    

    结果如下:

    当然也可以封装成Utils,以后需要执行什么任务就用这个后台线程的Looper机制去执行。

  • 对于理解Android的一些其他机制有帮助。

    比如HandlerThread其实就是帮我们封装了Looper,比如IntentService内部使用的是HandlerThread

    总之在理解了这个机制的原理之后,在遇到使用该机制的组件时理解起来就很轻松了!

总结

其实这篇文章存在的目的有几个:

  • 帮我自己梳理总结,并且以后忘记时可以快速回忆
  • 帮助读者快速理解该机制,并希望能够理解到设计该机制的出发点和一些原理细节的实现
  • 能帮助读者快速定位源码,更好地理解(因为其实我在看源码时也是参考了很多文章的,很多时候直接看源码遇到一个点卡住就好卡很久,没有带着问题和设计思路。这时如果有别人的文章作为基础,在他们的经验上去学习能节省很多时间。)

那么希望你理解该机制之后,除了能自己使用、理解一些Android的库之外;还能对这种线程交互机制、生产-消费者模式有一些更深的理解,比如说以后我们想要做一个有定时消息的队列,我们也会考虑用epoll去实现。这时举一反三,业务没有很复杂时,使用信号量的超时时间去替代epoll是否也可以?

还有一个,在理解了该机制之后,会发现Android源码其实很多时候并没有什么高大上的东西,以后我们有需要掌握的机制、组件自己看源码就可以了!

好了,这篇文章就到这里结束了,如果您有一些小小的收获我就心满意足了!感谢您的阅读。