第二节课_Handler|青训营笔记

简介: 在通常的情况下,我们会在子线程处理耗时操作,等待子线程耗时操作结束之后,再通知主线程更新UI。

线程通信_Handler

Handler 作用

在通常的情况下,我们会在子线程处理耗时操作,等待子线程耗时操作结束之后,再通知主线程更新UI。

Handler 是运行在主线程中的,它的作用就是帮助我们在子线程和主线程之间进行线程通信。我们可以在子线程中发送消息通知主线程,然后主线程中监听接收消息,进而对该消息进行处理。

1.webp.jpg

初识 Handler 的话,我们需要记住的是,他是一个不断在 looper 之中不断轮询的一个消息队列。我们可以不断的往这个轮询的池子中丢入信息,然后在获取的时候就是不断从池子里面的接受信息。

其基本用法也就是 发送接收

android.os.Handler handler = new Handler(Looper.getMainLooper()){
  @Override
  public void handleMessage(final Message msg) {
    //这里接受并处理消息
  }
};
//发送消息
handler.sendMessage(message);
handler.post(runnable);
复制代码
//简单示意
//子线程发消息
Thread {
            val msg = Message()
            msg.what = 1
            handler?.handleMessage(msg)
        }.start()
//主线程接收消息
handler = object : Handler(Looper.getMainLooper()){
            override fun handleMessage(message: Message){
                super.handleMessage(message)
                if (message.what == 1){
                    val TAG = "hhh"
                    Log.d(TAG, "handleMessage: ${message.what}")
                }
            }
        }
复制代码

Handler 浅析

  • Handler 与线程如何关联?
    Handler 在创建的时候是要先创建 Looper,就如上边的示意代码,创建 Handler 的时候,得传入一个 Looper 类型到构造器。

而Looper 使用 Looper.prepare() 方法来创建 Looper ,并借助 ThreadLocal 来实现与当前线程的绑定功能。

  • Handler 发出去的消息是谁管理的?
    我们查看 sendMessage(message) 或者 post(runnable) 可以得知,最后他们都会调用 MessageQueue.enqueueMessage(Message,long) 方法,即消息是由 MessageQueue 管理的
  • 消息又是怎么回到 handleMessage() 方法的?线程的切换是怎么回事?

Looper.loop() 是个死循环,会不断调用 MessageQueue.next() 获取 Message ,并调用 msg.target.dispatchMessage(msg) 回到了 Handler 来分发消息,以此来完成消息的回调

Thread.foo(){
  Looper.loop()
    //不断循环获取Message
   -> MessageQueue.next()
  //获取到之后,Message.target 就是发送该消息的 Handler,回到该Handler分发消息
   -> Message.target.dispatchMessage()
     -> Handler.handleMessage()
}
复制代码

Handler.handleMessage() 所在的线程最终由调用 Looper.loop() 的线程所决定

平时我们用的时候从异步线程发送消息到 Handler,这个 Handler 的 handleMessage() 方法是在主线程调用的,所以消息就从异步线程切换到了主线程。这就是线程的切换

1.webp.jpg1.webp.jpg


小结

  • Looper :负责关联线程以及消息的分发,在调用 Looper 线程下从 MessageQueue 获取 Message,分发给 Handler ;
  • MessageQueue :一个消息队列,负责消息的存储与管理,负责管理由 Handler 发送过来的 Message ;
  • Handler : 负责发送并处理消息,面向开发者,提供 API,并隐藏背后实现的细节。

Handler 发送的消息由 MessageQueue 存储管理,并由 Loopler 负责回调消息到 handleMessage()。

线程的转换由 Looper 完成,handleMessage() 所在线程由 Looper.loop() 调用者所在线程决定。

具体查看Handler 都没搞懂,拿什么去跳槽啊? - 掘金 (juejin.cn)

Handler 泄露问题

为什么 Handler 会引发泄露呢?

其主要原因是,我们错误的使用了 Handler ;这导致我们在关闭 Activity 的时,Handler 或者其他还在运行的类仍引用了 Activity 。这就导致 Activity 无法被回收,就会发生泄露。

有可到达 Activity 的引用链,可能会导致泄露的情况:

  • Thread 或者 Handler 为匿名内部类,持有了外部的 Activity
  • Thread 没了,但是其运行的 Message 还在发送。这会导致 Looper 也间接持有 Activity

GC root原理:通过对枚举GCroot对象做引用可达性分析,即从GC root对象开始,向下搜索,形成的路径称之为 引用链。如果一个对象到GC roots对象没有任何引用,没有形成引用链,那么该对象等待GC回收。

GC root对象是什么?

Java中可以作为GC Roots的对象

1、虚拟机栈(javaStack)(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

2、方法区中的类静态属性引用的对象。

3、方法区中常量引用的对象。

4、本地方法栈中JNI(Native方法)引用的对象。

解决 Handler 泄露

  1. 强应用 Activity 改为弱引用
  2. 及时切断两大 GC Root 的引用链关系:  Main Looper 到 Message;以及结束子线程。

1.webp.jpg

步骤1:Handler设为静态内部类,且对 Activity 弱引用

private static class SafeHandler extends Handler {
    private WeakReference<HandlerActivity> ref;
    public SafeHandler(HandlerActivity activity) {
        this.ref = new WeakReference(activity);
    }
    @Override
    public void handleMessage(final Message msg) {
        HandlerActivity activity = ref.get();
        if (activity != null) {
            activity.handleMessage(msg);
        }
    }
}
复制代码

步骤2:onDestroy() 的时候切断引用链关系

//1.若子线程任务尚未结束,及时中断
@Override
public void onDestroy() {
    ...
    thread.interrupt();
}
//子线程中创建了 Looper 并成为了 Looper 线程的话,须手动 quit
@Override
public void onDestroy() {
    ...
    handlerThread.quitSafely();
}
//主线程的 Looper 不可quit,退出App就挂掉了,所以要清除所有未处理的 message
@Override
public void onDestroy() {
    ...
    mainHandler.removeCallbacksAndMessages(null);
}
复制代码

小结

Handler 处理中持有 Activity 的,其生命周期应当与 Activity 一致,才是正确的用法

最正确用法应该是:

  1. 使用 Handler 机制,采用弱引用 + 静态内部类。保证错误延长了周期也能正确 GC
  2. Activity结束时候,清空 Message、终止 Thread 或退出 Looper。及时切除引用链

正确在子线程使用 Toast

由于 Toast 就是依赖于 Handler 完成的,所以在子线程中需要学习完整的 Handler 使用方式

//完整的需要有下面 1 2 两步
class LooperThread extends Thread {
    public Handler mHandler;
    public void run() {
        Looper.prepare();//1
        mHandler = new Handler() {
            public void handleMessage(Message msg) {
                // process incoming messages here
            }
        };
        Looper.loop();//2
    }
}
复制代码
//得出下列代码
new Thread(new Runnable() {
  @Override
  public void run() {
    Looper.prepare();
    Toast.makeText(HandlerActivity.this, "不会崩溃啦!", Toast.LENGTH_SHORT).show();
    Looper.loop();
  }
}).start();
复制代码

Looper一直死循环,为啥主线程不会卡死

死循环是为了保证主线程能一直执行下去,但是事实上他并不是简单的无限循环下去,而是会休眠的。当没有消息的时候,Handler会开始休眠,直到有消息传递的时候就会唤醒它。大部分情况下,Handler都是在休眠中。详细分析可以看Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

相关文章
|
8月前
|
存储 安全 网络协议
小白带你学习LinuxSSH服务(二十三)
小白带你学习LinuxSSH服务(二十三)
50 0
|
8月前
|
Python
1轻松学python第一节到第五节
1轻松学python第一节到第五节
37 0
|
Java 对象存储
2022年Servlect第十九课——案例实战
2022年Servlect第十九课——案例实战
39 0
|
存储 自然语言处理 算法
第01/90步《番外篇》第1章认识计算机世界第1课~第4课
今天学习《番外篇》第1章认识计算机世界的第1课~第4课内容,了解计算机基础原理及基础概念。没有练习,完成阅读并理解即可。
73 0
|
JavaScript
第86/90步《番外篇》第7章 学习编码规范 第39课
天学习《番外篇》第7章 学习编码规范 第39课 JS 编码补充注意事项
47 0
|
前端开发
第87/90步《番外篇》第7章 学习编码规范 第40课
今天学习《番外篇》第7章 学习编码规范 第40课 CSS 编写规范
44 0
|
JavaScript
第83/90步《番外篇》第7章 学习编码规范 第36课
今天学习《番外篇》第7章 学习编码规范 第36课 JS 基础编码规范
56 0
|
前端开发
第88/90步《番外篇》第7章 学习编码规范 第41课
今天学习《番外篇》第7章 学习编码规范 第41课 CSS 代码格式化规范
45 0
|
JavaScript
第84/90步《番外篇》第7章 学习编码规范 第37课
今天学习《番外篇》第7章 学习编码规范 第37课 JS 代码格式化规范
67 0
第90/90步《番外篇》第7章 学习编码规范 第43课
今天学习《番外篇》第7章 学习编码规范 第43课 HTML 语义使用规范
44 0