Android Handler使用介绍

news2024/9/21 22:18:27

        Android 中的 Handler 是用来和线程通信的重要工具。它主要用于在后台线程中执行任务,并将结果传递回主线程以更新用户界面。

一、基本概念

  • 线程间通信: Android 应用通常具有主线程(也称为 UI 线程)和后台线程。Handler 允许您从后台线程向主线程发送消息,从而在更新 UI 时避免出现线程安全问题。
  • 消息队列: Handler 使用消息队列来存储消息。每个消息都可以包含一个数据包,用于在线程之间传递信息。

        Handler 不仅仅用于将消息发送到主线程,您还可以在不同的线程之间进行通信。您可以在某个线程上创建 Handler,并将其传递给其他线程以便它们能够向该 Handler 发送消息。

1、使用案例

  • 在后台线程执行任务后更新 UI。
  • 在网络请求完成后通知 UI 更新。
  • 实现定时器功能,定期执行某个任务。

2、注意事项

  • Handler 与线程安全相关。确保您理解线程之间的通信机制以避免出现竞态条件或死锁。
  • 避免在 Handler 中进行耗时操作,以免阻塞主线程。

        总的来说,Handler 是 Android 开发中非常有用的工具,用于实现多线程通信和管理 UI 线程的消息处理。它为开发者提供了一种简单而有效的方法来处理异步任务和更新用户界面。 

二、使用方法

1、创建Handler

        您可以通过以下方式创建一个 Handler:

Handler handler = new Handler();

        这将创建一个与当前线程的 Looper 相关联的 Handler。

2、发送消息

        要向 Handler 发送消息,可以使用以下方法:

handler.sendMessage(Message msg);

        其中 Message 对象包含您要传递的信息。

3、处理消息

        要在 Handler 中处理消息,您需要重写 handleMessage(Message msg) 方法。例如:

Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        // 处理收到的消息
    }
};

4、延迟消息

        Handler 还允许您发送延迟消息,可以使用以下方法:

handler.sendMessageDelayed(Message msg, long delayMillis);

        这样可以在指定的延迟时间后将消息发送到消息队列中。

5、移除消息

        有时候您可能需要在某个时刻取消将要发送的消息。您可以使用以下方法:

handler.removeMessages(int what);

        这将移除具有特定标识符的所有消息。

三、子线程创建Handler

        我们平时使用 Handler 都是在主线程中 new Handler(),那么在子线程中也可以这样创建Handler吗?

private Handler threadHandler;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
 
    new Thread(new Runnable() {
        @Override
        public void run() {
            threadHandler = new Handler(new Handler.Callback() {
                @Override
                public boolean handleMessage(@NonNull Message msg) {
                    return false;
                }
            });
        }
    }).start();
}

        了解 Handler 原理的都知道,Handler 的消息处理是通过 Looper.loop() 里的死循环,不断的从消息队列中取出消息并处理,在主线程中已经自动调用了 Looper.loop() 方法,所以我们可以直接使用 new Handler() 创建,而在子线程中需要我们手动创建。

private Handler threadHandler;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
 
    new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            threadHandler = new Handler(new Handler.Callback() {
                @Override
                public boolean handleMessage(@NonNull Message msg) {
                    return false;
                }
            });
            Looper.loop();
        }
    }).start();
}

          上面的代码没有问题,但我们都是使用 handler.sendManager() 发送消息,在其他地方我们怎么创建该子线程对应的 handler 去发送消息呢?因为无法拿到子线程的 Looper,所以无法创建。

        下面我们创建一个自定义子线程,提供获取 Looper 方法:

public class MyHandlerThread extends Thread {
    Looper looper;

    MyHandlerThread(String name){
        super(name);
    }

    @Override
    public void run() {
        super.run();
        Looper.prepare();
        looper = Looper.myLooper();
        Looper.loop();
    }

    Looper getLooper(){
        return looper;
    }

    @Override
    public synchronized void start() {
        super.start();
    }
}

        使用自定义 Thread 创建 handler。

MyHandlerThread handlerThread = new MyHandlerThread("cx");
handlerThread.start();

Handler handler = new Handler(handlerThread.getLooper());

//子线程使用完成一定要退出,防止内存溢出
handler.getLooper().quit();

        上面方法中 handlerThread.getLooper() 可能存在空指针异常,以为 run() 为异步加载。所以必须要保证 run() 执行完,再去执行 getLooper() 方法,可以使用 wait() 和 notify() 实现。

public class MyHandlerThread extends Thread {
    Looper looper;

    MyHandlerThread(String name){
        super(name);
    }

    @Override
    public void run() {
        super.run();
        Looper.prepare();
        synchronized (this) {
            looper = Looper.myLooper();
            notifyAll(); //唤醒wait()
        }
        Looper.loop();
    }

    Looper getLooper(){
        synchronized (this) {
            while (looper == null) {//用while,防止被其他notify唤醒,重新判断
                try {
                    wait(); //释放锁,并等待
                } catch (InterruptedException e) {
                }
            }
        }
        return looper;
    }

    @Override
    public synchronized void start() {
        super.start();
    }
}

wait():CPU 释放,线程继续做别的事情。

sleep():睡眠,线程停止等待。

        上面主要是为了理解 wait()、notify() 和 synchronized() 的使用,其实 MyHandlerThread 已经帮我们实现好了,直接使用 HandlerThread 即可。

四、Handler相关问答

1、用一句话概括Handler,并简述其原理

        Handler 是 Android 系统的根本,在 Android 应用被启动的时候,会分配一个单独的虚拟机,虚拟机会执行 ActivityThread 中的 main 方法,在 main 方法中对主线程 Looper 进行了初始化,也就是几乎所有代码都执行在 Handler 内部。Handler 也可以作为主线程和子线程通讯的桥梁。 Handler 通过 sendMessage 发送消息,将消息放入 MessageQueue 中,在 MessageQueue 中通过时间的维度来进行排序,Looper 通过调用 loop 方法不断的从 MessageQueue 中获取消息,执行 Handler 的 dispatchMessage,最后调用 handleMessage 方法。

2、为什么系统不建议在子线程访问UI(为什么不能在子线程更新UI)

        在某些情况下,在子线程中是可以更新 UI 的。但是在 ViewRootImpl 中对 UI 操作进行了 checkThread,但是我们在 onCreate 和 onResume 中可以使用子线程更新UI,由于我们在 ActivityThread 中的 performResumeActivity 方法中通过 addView 创建了 ViewRootImpl,这个行为是在 onResume 之后调用的,所以在 onCreate 和 onResume 可以进行更新UI。
        但是我们不能在子线程中更新 UI,因为 UI 更新本身是线程不安全的,如果添加了耗时操作之后,一旦 ViewRootImpl 被创建将会抛出异常。一旦在子线程中更新 UI,容易产生并发问题。

3、一个Thread可以有几个Looper,几个Handler

        一个线程只能有一个 Looper,可以有多个 Handler。

  • 一个Looper:因为Looper需要循环,循环后面的代码无法执行了,所以一个线程只有一个Looper
  • 多个Handler:为了相互独立,互不干扰,各自处理各自的消息,谁发生的Message谁处理,也可以通过removeMessages清空掉自己的Message。

        线程在初始化时候调用 Looper.prepare() 方法对将静态对象 ThreadLocal 作为 key(整个进程全部线程的 Looper 共用一个 sThreadLocal),创建 Looper 对象作为 value 存储在当前线程的 ThreadLocalMap 中(其实就只有一个 key 对应 value,key 为 sThreadLocal、value 为 looper 对象。这两只有一个,所以 ThreadLocalMap 只有一个键值对)。

        通过线程独有的 ThreadLocal 实现将 Looper 存储在 ThreadLocalMap 这样键值对的数据结构中。

4、Message是如何创建的

        首先考虑一个问题,屏幕刷新率 60Hz(即每秒刷新60次),每次刷新要用到 3 个 Message,也就是每秒钟至少要创建 180 个 Message。这样不断的创建回收,就会出现内存抖动的问题,从而导致 GC、屏幕卡顿等问题。

        为了解决上面的问题,采用 Message 了享元的设计模式,使用 obtain() 方法创建。在Handler 中创建两个线程池队列,一个是我们比较熟悉的 MessageQueue,另一个就是回收池 sPool(最大长度是 50 复用池)。MessageQueue 中 Message 回收时,我们将清空数据的 Message 放回到 sPool 队列中。创建 Manager,我们直接从 sPool 池中取出来就可以了。

        应用场景:地图、股票、RecyclerView复用等对数据的处理都使用了享元模式。

5、主线程中Looper的轮询死循环为何没有阻塞主线程

        Looper 轮询是死循环,但是当没有消息的时候他会 block(阻塞, 阻塞代码没有执行计时操作),ANR 是当我们处理点击事件的时候 5s 内没有响应,我们在处理点击事件的时候也是用的 Handler,所以一定会有消息执行,并且 ANR 也会发送 Handler 消息,所以不会阻塞主线程。Looper 是通过 Linux 系统的 epoll 实现的阻塞式等待消息执行(有消息执行无消息阻塞),而 ANR 是消息处理耗时太长导致无法执行剩下需要被执行的消息触发了 ANR。

6、ANR机制

         启动 Service 为例,APP进程 startService,system_service 进程通知 ActivityManager开始计时,同时 Service 进程开始启动,启动完成后会发送消息到 ActivityManager。

        如果时前台 Service,ActivityManager 20秒没有收到启动完成的消息就会产生ANR。

7、使用Hanlder的postDealy()后消息队列会发生什么变化

        Handler 发送消息到消息队列,消息队列是一个时间优先级队列,内部是一个单向链表。发动 postDelay 之后会将该消息进行时间排序存放到消息队列中。

8、点击页面上的按钮后更新TextView的内容,谈谈理解

        点击按钮的时候会发送消息到 Handler,但是为了保证优先执行,会加一个标记异步,同时会发送一个 target 为 null 的消息,这样在使用消息队列的 next 获取消息的时候,如果发现消息的 target 为 null,那么会遍历消息队列将有异步标记的消息获取出来优先执行,执行完之后会将 target 为 null 的消息移除。(同步屏障:拦截同步消息执行,优先执行异步消息。 View 更新时,draw、requestLayout、invalidate 等很多地方都调用异步消息的方法)。

        同步屏障的设置可以方便地处理那些优先级较高的异步消息。当我们调用 Handler.getLooper().getQueue().postSyncBarrier() 并设置消息的setAsynchronous(true)时,target 即为 null ,也就开启了同步屏障。当在消息轮询器 Looper 在 loop() 中循环处理消息时,如若开启了同步屏障,会优先处理其中的异步消息,而阻碍同步消息。

9、生产者-消费者设计模式

        在生产者-消费者模式中如果消费者消费能力大于生产者,或者生产者生产能力大于消费者,需要一个限制,在 java 里有一个 blockingQueue。当目前容器内没有东西的时候,消费者来消费的时候会被阻塞,当容器满了的时候也会被阻塞。

        这里 Handler.sendMessage 相当于一个生产者,MessageQueue 相当于容器,Looper 相当于消费者。Handler 不使用 java 的 blockingQueue 的原因是除了我们上层需要使用到 Handler,其实底层的消息都是需要传递给 Handler 处理,比如:驱动层需要发事件给APP、屏幕点击事件、底层刷新通知等,所以我们使用的是 Native 层提供的 MessageQueue 实现消息队列。

10、Handler是如何完成子线程和主线程通信的

        在主线程中创建 Handler,在子线程中发送消息,放入到 MessageQueue 中,通过 Looper.loop 取出消息进行执行 handleMessage,由于 looper 我们是在主线程初始化的,在初始化 looper 的时候会创建消息队列,所以消息是在主线程被执行的。

11、关于ThreadLocal

        ThreadLocal 实例进程内只有一个(静态实例),但其内部的 set 和 get 方法是获取的当前线程的 ThreadLocalMap 对象,ThreadLocalMap 是每个线程有一个单独的内存空间,不共享,ThreadLocal 在 set 的时候会将数据存入对应线程的 ThreadLocalMap中,key = ThreadLocal,value = 值。

12、Handler内存泄漏问题及解决方案

        内部类持有外部类的引用导致了内存泄漏,如果 Activity 退出的时候,MessageQueue中还有一个 Message 没有执行,这个 Message 持有了 Handler 的引用,而 Handler 持有了 Activity 的引用,导致 Activity 无法被回收,导致内存泄漏。使用 static 关键字修饰,在 onDestory 的时候将消息清除。

        简单理解,当 Handler 为非静态内部类时,其持有外部类 Actviity 对象,所以导致 static Looper -> mMainLooper -> MessageQueue -> Message -> Handler -> MainActivity,这个引用链中 Message 如果还在 MessageQueue 中等待执行,则会导致 Activity 一直无法被释放和回收。 

        导致的原因是因为 Looper 需要循环,所以一个线程只有一个 Looper,但一个线程中可有多个 Handler,MessageQueue 中消息 Message 执行时不知道要通知哪个 Handler 执行任务,所以在 Message 创建时中存入了 Handler 对象 target 用于回调执行的消息。如果 Handler 是 Activity 这种短生命周期对象的非静态内部类时,则会让创建出来的 Handler 对象持有该外部类 Activity 的引用,Message 还在队列中导致引用着 Handler,而非静态内部类 Handler 引用外部类 Activity 导致 Activity 无法被回收,最终导致内存泄漏。

        解决办法:

  • Handler 不能是 Activity 这种短生命周期的对象类的内部类;
  • 在 Activity 销毁时,将创建的 Handler 中的消息队列清空并结束所有任务。
  • 将 handler 设置成 static,static 变量是全局变量,不能够自动引用外部类变量,这时Handler 就不再持有 MainActivity,MainActivity 就可以正常释放。

13、Handler异步消息处理(HandlerThread)

        内部使用了 Handler+Thread,并且处理了getLooper 的并发问题。如果获取 Looper 的时候发现 Looper 还没创建,则 wait,等待 looper 创建了之后在 notify。

14、子线程中的Looper,无消息时如何处理

        子线程中创建了 Looper,当没有消息的时候子线程将会被 block,无法被回收,所以我们需要手动调用 quit 方法将消息删除并且唤醒 looper,然后 next 方法返回 null 退出 loop。

15、多个Handler往MessageQueue中添加数据(Handler可能处于不同线程),如何确保线程安全

        在添加数据和执行 next 的时候都加了 this 锁,这样可以保证添加的位置是正确的,获取的也会是最前面的。

16、关于IntentService

        HandlerThread + Service 实现,可以实现 Service 在子线程中执行耗时操作,并且执行完耗时操作时候会将自己 stop。

17、Glide如何维护生命周期

        一般想问的应该都是这里为什么会判断两次 null :

@NonNull
private RequestManagerFragment getRequestManagerFragment(@NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) {
    RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
    if (current == null) {
        current = pendingRequestManagerFragments.get(fm);
        if (current == null) {
            current = new RequestManagerFragment();
            current.setParentFragmentHint(parentHint);
            if (isParentVisible) {
                current.getGlideLifecycle().onStart();
            }
            pendingRequestManagerFragments.put(fm, current);
            fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
            handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
        }
    }
    return current;
}

1)在多次调用 with 的时候,commitAllowingStateLoss 会被执行两次,所以我们需要使用一个 map 集合来判断,如果 map 中已经有了证明已经添加过了。
2)handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget(),我们需要将 map 里面的记录删除。

18、handler主线程阻塞了怎么办

        Android 系统事件驱动系统,loop 循环处理事件,如果不循环程序就结束了。

19、Handler底层为什么用epoll,为什么不用select和poll

        Socket 非阻塞 IO 中 select 需要全部轮询不适合,poll 也是需要遍历和 copy,效率太低了。epoll 非阻塞式 IO,使用句柄获取 APP 对象,epoll 无遍历,无拷贝。还使用了红黑树(解决查询慢的问题)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1536398.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

有些商标名称慎加通用词,可能会以误认驳回!

近期看到一网友在30类方便食品申请"某某茶叶",这个商标名称部分商品通过一部分,另一部分商品以误认驳回,普推知产老杨分析时发现在以前很少出现这种情况,但是近年来商标名称加通用词误认驳回的比较多。 现阶段这种带通…

深度解析ThreadLocal:底层原理、数据隔离与内存泄漏解决

前言 这个问题算是我的一个羞耻点,起源于一次面试中,面试官问ThreadLocal的底层实现是啥,我那时候一直以为ThreadLocal是一个类似于Redis一样的独立于线程外的第三方存储容器,如何底层维护了一个Map结构,以线程ID为Key…

专题一——双指针算法

原理:将数组进行区间划分,通过指针(下标)的移动实现题目所要求的区间(数组分块) (实现代码统一是C) 建议在做题与看题解时要自己反复模拟这个实现的过程,以后在做题做到类似的题才能举一反三&am…

QT6实现创建与操作sqlite数据库及读取实例(一)

一.Qt为SQL数据库提供支持的基本模块(Qt SQL) Qt SQL的API分为不同层: 驱动层 SQL API层 用户接口层 1.驱动层 对于Qt 是基于C来实现的框架,该层主要包括QSqlDriver,QSqlDriverCreator,QSqlDriverCreatorBase,QSqlPlug…

Linux第78步_使用原子整型操作来实现“互斥访问”共享资源

使用原子操作来实现“互斥访问”LED灯设备,目的是每次只允许一个应用程序使用LED灯。 1、创建MyAtomicLED目录 输入“cd /home/zgq/linux/Linux_Drivers/回车” 切换到“/home/zgq/linux/Linux_Drivers/”目录 输入“mkdir MyAtomicLED回车”,创建MyA…

Python从 Google 地图空气质量 API 获取空气污染数据

获取给定位置当前的空气质量 让我们开始吧!在本节中,我们将介绍如何使用 Google 地图获取给定位置的空气质量数据。您首先需要一个 API 密钥,可以通过您的 Google Cloud 帐户生成该密钥。他们有90 天的免费试用期,之后您将为您使用的 API 服务付费。在开始大量拨打电话之前…

51单片机中断信号的种类及应用场景

在嵌入式系统中,中断是一种重要的事件处理机制,它可以在程序执行的任何时候暂停当前任务,转而执行与之相关的特殊任务或事件。51单片机作为一种常见的微控制器,其中断功能在各种应用中起着关键作用。然而,对于初学者和…

一、SpringBoot基础搭建

本教程主要给初学SpringBoot的开发者,通过idea搭建单体服务提供手把手教学例程,主要目的在于理解环境的搭建,以及maven模块之间的整合与调用 源码:jun/learn-springboot 以商城项目为搭建例子,首先计划建1个父模块&…

部署单节点k8s并允许master节点调度pod

安装k8s 需要注意的是k8s1.24 已经弃用dockershim,现在使用docker需要cri-docker插件作为垫片,对接k8s的CRI。 硬件环境: 2c2g 主机环境: CentOS Linux release 7.9.2009 (Core) IP地址: 192.168.44.161 一、 主机配…

GPT-4 VS Claude3、Gemini、Sora:五大模型的技术特点与用户体验

【最新增加Claude3、Gemini、Sora、GPTs讲解及AI领域中的集中大模型的最新技术】 2023年随着OpenAI开发者大会的召开,最重磅更新当属GPTs,多模态API,未来自定义专属的GPT。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义,不亚…

CTF题型 md5考法例题汇总

CTF题型 md5考法相关例题总结 文章目录 CTF题型 md5考法相关例题总结一.md5弱字符相等()[SWPUCTF 2021 新生赛]easy_md5 二.md5强字符相等()1)文件相等[2024 qsnctf 擂台赛 easy_md5]2)字符相等[安洵杯 2019]easy_web 三.md5哈希长度扩展攻击[NPUCTF2020]ezinclude文件包含利用…

深入技术细节:放弃Spring Security,自己实现Token权限控制!

最近做了个项目,大家都知道很多的项目都是在自己手上原本的框架内进行业务开发。但是甲方爸爸的这个项目需要交付原代码,并且要求框架逻辑简单清晰,二次开发简易上手。 那不是要重新从0到1写一套框架吗? 试着先给甲方爸爸报一下…

美食杂志制作秘籍:引领潮流,引领味蕾

美食杂志是一种介绍美食文化、烹饪技巧和美食体验的杂志,通过精美的图片和生动的文字,向读者展示各种美食的魅力。那么,如何制作一本既美观又实用的美食杂志呢? 首先,你需要选择一款适合你的制作软件。比如FLBOOK在线制…

Java微服务分布式事务框架seata的TCC模式

🌹作者主页:青花锁 🌹简介:Java领域优质创作者🏆、Java微服务架构公号作者😄 🌹简历模板、学习资料、面试题库、技术互助 🌹文末获取联系方式 📝 往期热门专栏回顾 专栏…

教大家使用vue实现 基础购物车。

首先我要知道一点 其能够数据变化的 都用{{}}来进行渲染类似 来看一下实现效果 实现思路 : 1,引进 vue.js 2,setup 将声明变量 方法放在setup里面 3,用响应式声明 ref() 或rective声明 可以声明对象等等 let 也…

AST学习入门

AST学习入门 1.AST在线解析网站 https://astexplorer.net/ 1.type: 表示当前节点的类型,我们常用的类型判断方法t.is********(node)**,就是判断当前的节点是否为某个类型。 2**.start**:表示当前节点的开始位置 3.end:当前节点结束 4.loc : 表示当前节点所在的行…

产品推荐 | 基于Xilinx FPGA XC5VFX100T的6U VPX视频叠加板卡

01、产品概述 本板卡是基于Xilinx FPGA XC5VFX100T的6U VPX视频叠加板卡。主要用于视频叠加板具有多种高清图形输入接口,可实现其中两路高清视频信号的开窗显示和叠加显示功能;或者输出和输入图形接口的转换。 02、物理特性 ● 尺寸:6U CPC…

【文献阅读】AlphaFold touted as next big thing for drug discovery — but is it?

今天来精读2023年10月发在《Nature》上的一篇新闻:AlphaFold touted as next big thing for drug discovery — but is it? (nature.com)https://www.nature.com/articles/d41586-023-02984-w Questions remain about whether the AI tool for predicting protein …

Python Windows系统 虚拟环境使用

目录 1、安装 2、激活 3、停止 1、安装 1)为项目新建一个目录(比如:目录命名为learning_log) 2)在终端中切换到这个目录 3)执行命令:python -m venv ll_env,即可创建一个名为ll…

常用的6个的ChatGPT网站,国内可用!

GPTGod 🌐 链接: GPTGod 🏷️ 标签: GPT-4 免费体验 支持API 支持绘图 付费选项 📝 简介:GPTGod 是一个功能全面的平台,提供GPT-4的强大功能,包括API接入和绘图支持。用户可以选择免…