• 进阶之路 | 奇妙的Handler之旅


    前言

    本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:

    我的GIthub博客

    本文已授权公众号顾林海发布:https://mp.weixin.qq.com/s/HatMCXW-ErkPb4GWkMoBvA

    需要已经具备的知识:

    • Handler的基本概念及使用

    学习导图:

    学习导图

    一.为什么要学习Handler?

    Android平台上,主要用到的通信机制有两种:HandlerBinder,前者用于进程内部的通信,后者主要用于跨进程通信。

    在多线程的应用场景中,Handler将工作线程中需更新UI的操作信息 传递到 UI主线程,从而实现工作线程对UI的更新处理,最终实现异步消息的处理。

    作为一个Android程序猿,知其然而必须知其所以然,理解其源码能更好地了解Handler机制的原理。下面,我就从消息机制入手,带大家畅游在Handler的世界中,体会Google工程师的智慧之光。

    二.核心知识点归纳

    2.1 消息机制概述

    A.作用:跨线程通信

    B.常用场景:当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行

    • 系统不建议在子线程访问UI的原因:UI控件非线程安全,在多线程中并发访问可能会导致UI控件处于不可预期的状态

    • 而不对UI控件的访问加上机制的原因有:

      1.上锁会让UI控件变得复杂和低效

      2.上锁后会阻塞某些进程的执行

    C.四要素:

    • Message:需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理
    • MessageQueue:用来存放Handler发送过来的消息,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
    • Handler:负责Message的发送及处理
    • Handler.sendMessage():向消息队列发送各种消息事件
    • Handler.handleMessage() 处理相应的消息事件
    • Looper:通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者,可以看成是消息泵

    Thread:负责调度整个消息循环,即消息循环的执行场所

    存在关系:

    • 一个Thread只能有Looper,可以有Handler
    • LooperMessageQueue,可以处理来自HandlerMessage
    • MessageQueue有一组待处理的Message,这些Message可来自不同的Handler
    • Message中记录了负责发送和处理消息的Handler
    • Handler中有LooperMessageQueue

    关系图

    数量关系

    D.使用方法:

    • ActivityThread主线程实例化一个全局的Handler对象
    • 在需要执行UI操作的子线程里实例化一个Message并填充必要数据,调用Handler.sendMessage(Message)方法发送出去
    • 重写handleMessage()方法,对不同Message执行相关操作

    E.总体工作流程:

    这里先总体地说明一下Android消息机制的工作流程,具体的ThreadLocal,MessageQueue,Looper,Handler的工作原理会在下文详细解析

    • Handler.sendMessage()发送消息时,会通过MessageQueue.enqueueMessage()MessageQueue中添加一条消息
    • 通过Looper.loop()开启循环后,不断轮询调用MessageQueue.next()
    • 调用目标Handler.dispatchMessage()去传递消息,目标Handler收到消息后调用Handler.handleMessage()处理消息

    简单来看,即HandlerMessage发送到Looper的成员变量MessageQueue中,之后Looper不断循环遍历MessageQueue从中读取Message,最终回调给Handler处理。如图:

    总体工作流程

    2.2 消息机制分析

    2.2.1 ThreadLocal

    了解ThreadLocal,有助于我们后面对Looper的探究

    Q1:ThreadLocal是什么

    首先我们来看一下官方源码(Android 9.0

    This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

    大致意思:

    ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,只有在指定线程中才能获取到存储的数据(也就是说,每个线程的一个变量,有自己的值)

    Q2:ThreadLocal使用场景

    • 当某些数据是以线程为作用域且每个线程特有数据副本
    • Android中具体的使用场景:Looper,ActivityThread,AMS

    • 如果不采用ThreadLocal的话,需要采取的措施:提供一个全局哈希表

    • 复杂逻辑下的对象传递,比如:监听器的传递
    • 采用ThreadLocal让监听器作为线程中的全局对象,线程内部只有通过get方法即可得到监听器

    • 如果不采用ThreadLocal的方案:

      a.将监听器作为参数传递

      缺点:当调用栈很深的时候,程序设计看起来不美观

      b.将监听器作为静态变量

      缺点:状态不具有可扩充性

    Q3:ThreadLocalsynchronized的区别:

    • 对于多线程资源共享的问题,synchronized机制采用了“以时间换空间”的方式
    • ThreadLocal采用了“以空间换时间”的方式
    • 前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响,所以ThreadLocalsynchronized都能保证线程安全,但是应用场景却大不一样。

    Q4:原理

    ThreadLocal主要操作为set,get操作,下面分别介绍流程

    A1:set的原理

    set流程图

    A2:get的原理

    get流程图

    综上所述,ThreadLocal之所以有这么奇妙的效果,是因为:

    • 不同线程访问同一个ThreadLocal.get(),其内部会从各种线程中取出对应线程的table数组,然后根据当前ThreadLocal的索引查找出对应的values

    想要了解ThreadLocal具体源码的读者,推荐一篇文章:ThreadLocal详解

    2.2.2 MessageQueue

    • 数据结构:MessageQueue的数据结构是单链表

    • 操作:

      A.enqueueMessage

      主要操作是单链表的插入操作

      B.next

      是一个无限循环的方法,如果没有消息,会一直阻塞;当有消息的时候,next会返回消息并将其从单链表中移出

    2.2.3 Looper

    Q1:Looper的作用

    • 作为消息循环的角色
    • 它会不停地从MessageQueue中查看是否有新消息,若有新消息则立即处理,否则一直阻塞(不是ANR
    • Handler需要Looper,否则将报错
    • Handler内部通过ThreadLocal获取到当前线程的Looper

    Q2:Looper的使用

    a1:开启:

    UI线程会自动创建Looper,子线程需自行创建

    //子线程中需要自己创建一个Looper
    new Thread(new Runnable() {
                @Override
                public void run() {
                    Looper.prepare();//为子线程创建Looper   
                    Handler handler = new Handler();
                    Looper.loop(); //开启消息轮询
                }
            }).start();
    
    • 除了prepare(),还提供prepareMainLooper(),本质也是通过prepare()
    • getMainLooper() 作用:获取主线程的Looper

    a2:关闭:

    • quit:直接退出
    • quitSafely:设定退出标记,待MessageQueue中处理完所有消息再退出

    退出Looper的话,子线程会立刻终止;因此:建议在不需要的时候终止Looper

    Q3:原理:

    Looper原理

    2.2.4 Handler

    Q1:Handler的两种使用方式:

    注意:创建Handler实例之前必须先创建Looper实例,否则会抛RuntimeException(UI线程自动创建Looper)

    • send方式
    //第一种:send方式的Handler创建
    Handler mHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    //如UI操作
    
                }
            };
    //send
    mHandler.sendEmptyMessage(0);
    
    • post方式

    最终是通过一系列send方法来实现

    //实例化Handler
    private Handler mHandler = new Handler();
    //这里调用了post方法,和sendMessage一样达到了更新UI的目的
         mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mTextView.setText(new_str);
                }
            });
    

    Q2:Handler处理消息过程

    Handler发送消息流程

    一张图总结Handler

    2.3 Handler 的延伸

    2.3.1 内存泄露

    在初学Handler的时候,往往会发现AS亮起一大块黄色,以警告可能会发生内存泄漏

    Handler警告

    • 发生场景:Handler 允许我们发送延时消息,如果在延时期间用户关闭了Activity,那么该Activity会泄露
    • 原因:这个泄露是因为因为 Java 的特性,内部类会持有外部类ActivityHandler 持有引用,HandlerMessage持有引用,而MessageMessageQueue持有引用,而MessageQueue是属于TLS(ThreadLocalStorage)线程,是与Activity不同的生命周期。所以当Activity的生命周期结束后,而MessageQueue中还存在未处理的消息,那么上面一连串的引用链就不允许Activity的对象被回收,就造成了内存泄漏

    即两个关键条件:

    • 存在Activity-->Handler-->Message-->MessageQueue的一连串引用链
    • Handler的生命周期 > 外部类的生命周期

    引用链

    • 解决方式:

      A.Activity销毁时,清空Handler中未执行或正在执行的Callback以及Message

        // 清空消息队列,移除对外部类的引用
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mHandler.removeCallbacksAndMessages(null);
        }
    

    ​ B.静态内部类+弱引用

    • 为了保证不再持有当前Activity的引用,我们采用静态内部类的方式
    • 为了让Handler在处理消息时调用外部类Activity的方法,且能在GC时回收其内存(换句话说:有短暂的生命周期),所以我们这里采用弱引用的方式
    
    private static class AppHandler extends Handler {
        //弱引用,在垃圾回收时,被回收
        WeakReference<Activity> mActivityReference;
    
        AppHandler(Activity activity){
            mActivityReference=new WeakReference<Activity>(activity);
        }
    
        public void handleMessage(Message message){
            switch (message.what){
                 HandlerActivity activity=mActivityReference.get();
                 super.handleMessage(message);
                if(activity!=null){
                    //执行业务逻辑
                   
                }
            }
        }
    }
    

    Java各种引用

    2.3.2 Handler里藏着的Callback

    首先看下Handler.dispatchMessage(msg)

    public void dispatchMessage(Message msg) {
      //这里的 callback 是 Runnable
      if (msg.callback != null) {
        handleCallback(msg);
      } else {
        //如果 callback 处理了该 msg 并且返回 true, 就不会再回调 handleMessage
        if (mCallback != null) {
          if (mCallback.handleMessage(msg)) {
            return;
          }
        }
        handleMessage(msg);
      }
    }
    

    可以看到 Handler.Callback优先处理消息的权利

    • 当一条消息被 Callback 处理并拦截(返回 true,那么 Handler.handleMessage(Msg) 方法就不会被调用了
    • 如果Callback处理了消息,但是并没有拦截,那么就意味着一个消息可以同时被Callback以及 Handler 处理

    这个就很有意思了,这有什么作用呢?

    我们可以利用 Callback 这个拦截机制来拦截 Handler 的消息!

    场景:Hook ActivityThread.mH ,笔者在进阶之路 | 奇妙的四大组件之旅介绍过ActivityThread,在 ActivityThread中有个成员变量 mH ,它是个 Handler,又是个极其重要的类,几乎所有的插件化框架都使用了这个方法

    限于当前知识水平,笔者尚未研究过插件化的知识,以后有机会的话希望能给大家介绍!

    2.3.3 创建 Message 的最佳方式

    为了节省开销,尽量复用 Message ,减少内存消耗

    法一:Message msg=Message.obtain();

    法二:Message msg=handler.obtainMessage();

    2.3.4 妙用 Looper 机制

    我们可以利用Looper的机制来帮助我们做一些事情:

    • Runnable post 到主线程执行
    • 利用 Looper 判断当前线程是否是主线程
    public final class MainThread {
    
        private MainThread() {
        }
    
        private static final Handler HANDLER = new Handler(Looper.getMainLooper());
    
        //将 Runnable post 到主线程执行
        public static void run(@NonNull Runnable runnable) {
            if (isMainThread()) {
                runnable.run();
            }else{
                HANDLER.post(runnable);
            }
        }
    
        //判断当前线程是否是主线程
        public static boolean isMainThread() {
            return Looper.myLooper() == Looper.getMainLooper();
        }
    
    }
    
    

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

    这个是老生常谈的问题了,记得当初被学长问到这个问题的时候,一脸懵逼,然后胡说一通,实属羞愧

    要弄清这个问题,我们可以通过几个问题来逐层深入剖析

    Q1:什么是线程?

    线程是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出

    Q2:进入死循环是不是说明一定会阻塞

    前面也说到了线程既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出

    想到这就理解,主线程也是一个线程,它也要维持自己的周期,所以也是需要一个死循环的。所以死循环并不是那么让人担心。

    Q3:什么是Looper的阻塞?

    • Looper的阻塞,前提是没有输入事件,此时MessageQueue是空的,Looper进入空闲,线程进入阻塞,释放CPU,等待输入事件的唤醒
    • Looper阻塞的时候,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源

    Looper的阻塞涉及到Linux pipe/epoll机制,想了解的读者可自行Google

    Q4:聊聊ANR

    • 其实初学者很容易将ANRLooper的阻塞二者相混淆
    • UI耗时导致卡死,前提是要有输入事件,此时MessageQueue不是空的,Looper正常轮询,线程并没有阻塞,但是该事件执行时间过长(一般5秒),而且与此期间其他的事件(按键按下,屏幕点击..也是通过Looper处理的)都没办法处理(卡死),然后就ANR异常了

    Q5:卡死的真正原因:

    • 真正卡死的原因是:在回调方法onCreate/onStart/onResume等操作时间过长

    三.课堂小测试

    恭喜你!已经看完了前面的文章,相信你对Handler已经有一定深度的了解,下面,进行一下课堂小测试,验证一下自己的学习成果吧!PS:限于篇幅,笔者就不提供答案了,不过答案一搜就有了

    Q1:如何将一个Thread线程变成Looper线程?Looper线程有哪些特点

    Q2:简述下HandlerMessageLooper的作用,以及他们之间的关系

    Q3: 简述消息机制的回调处理过程,怎么保证消息处理机制的唯一性

    Q4:为什么发送消息在子线程,而处理消息就变成主线程了,在哪儿跳转的


    如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

    本文参考链接:

  • 相关阅读:
    Vue element 下拉框 可输入可选择(无bug)
    Spring Data JPA删除及批量删除功能 delete(list)和deleteInBatch(list) 是执行多条sql和条sql 的区别
    IDEA使用JPA自定义查询,报错Can‘t resolve symbol ‘Type‘ ,hql查询时使用的from Xxx,Xxx不是实体类的名称,而是EntityName(Hibernate注解),默认采用实体类的名称;若显示地指明了 EntityName,因此在使用hql查询的时候,要from EntityName,而不是from 实体类名
    Elementelrowelcol布局组件详解
    SQL INSERT INTO SELECT 语句 通过 SQL,您可以从一个表复制信息到另一个表。INSERT INTO SELECT 语句从一个表复制数据,然后把数据插入到一个已存在的表中。
    IDEA使用JPA自定义查询,报错Can‘t resolve symbol ‘Type‘
    JAVA 中 string 和 int 互相转化 1、 int i = Integer.parseInt([String]); 或 i = Integer.parseInt([String],[int radix]); 2、 int i = Integer.valueOf(my_str).intValue();
    ElementUI中Select选择器讲解 clearable和filterable和对应的钩子函数@change 可输入提示信息下拉框
    Java集合中移除所有的null值
    frp使用教程
  • 原文地址:https://www.cnblogs.com/xcynice/p/qi_miao_de_handler_zhi_lv.html
Copyright © 2020-2023  润新知