Android中的消息异步处理机制及实现方案

基本介绍

  • 当我们需要执行复杂的计算逻辑,网络请求等耗时操作时,服务器可能不会立即响应请求,如果不将这类操作放在子线程中运行,就会导致主线程被阻塞住,从而影响用户的使用体验
  • 如果想要更新应用程序中的UI控件,则必须在主线程中进行,否则就会出现android.view.ViewRootImpl$CalledFromWrongThreadException异常

代码实践

  • 有些时候,我们需要在子线程中执行一些耗时任务,再根据任务的执行结果来更新相应的UI控件,对此,Android提供了一套异步消息的处理机制,解决了在子线程中进行UI操作的问题,我们先来看看异步消息处理的使用方法,再来分析其中的原理
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.example.myapplication1.R;

import java.util.concurrent.TimeUnit;

public class EventHandlerActivity extends AppCompatActivity {

    private static final int CALCULATE_KEY = 2024;
    private Button calculateBtn;
    private TextView resultTv;

    // 创建Handler实例,用于在主线程中更新UI
    private Handler myHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            if (msg.what == CALCULATE_KEY) {
                resultTv.setText("Result: " + msg.obj);
            }

        }
    };
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_event_handler);

        calculateBtn = findViewById(R.id.calculateBtn);
        resultTv = findViewById(R.id.resultTv);

        calculateBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 点击按钮后,开始执行复杂计算
                calculateFunc();
            }
        });
    }

    private void calculateFunc() {
        // 创建一个新线程来执行耗时操作
        new Thread(new Runnable() {
            @Override
            public void run() {
                long result = factorial(5);
                // 模拟复杂的耗时计算
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 发送消息给Handler,以便在主线程中更新UI
                // 另外,为了避免频繁地创建和销毁 Message 对象,可以使用 Message.obtain() 方法从消息池中获取一个消息实例,以减少内存分配和垃圾回收的频率
                Message message = Message.obtain();
                message.what = CALCULATE_KEY;
                message.obj = "calculate result = " + result;
                myHandler.sendMessage(message);
                
                // sendMessageDelayed(Message msg, long delayMillis): 在指定的延迟时间后发送Messag, delayMillis为单位为毫秒
                // myHandler.sendMessageDelayed(message,5000);
            }
        }).start();

    }


    private long factorial(int num){
        if (num < 2) return 1;
        return num * factorial(num - 1);
    }
}
  • activity_event_handler.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".handle.EventHandlerActivity">

    <Button
        android:id="@+id/calculateBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Calculate Factorial"
        android:textAllCaps="false"/>

    <TextView
        android:id="@+id/resultTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/calculateBtn"
        android:layout_marginTop="16dp"/>

</RelativeLayout>
  • 如上代码,点击calculateBtn按钮,交由工作线程完成计算操作,将计算机结果显示在resultTv(交由主线程完成UI操作)

原理实现

  • Android中的异步消息处理主要由四部分组成:Message、Handler、MessageQueue和Looper,下面对这四部分来进行详细介绍

Message

  • Message是在线程之间传递的消息,可以在内部携带少量的数据,用于在不同线程间交换,其实例包含what
  • Message的what字段是一个整数值,可用来分区不同的消息类型,可为不同的任务或事件分配不同的what值,当调用Handler实例handleMessage方法时,可检查Message对象的what字段来确定如何处理该消息
  • arg1和arg2字段:均为整数类型字段,可携带what之外其他的整数类型数据
  • obj字段:Object类型字段,可携带字符串、数组、对象、Bundle等类型数据

Handler

  • Handler:处理者,主要用于发送和处理消息,发送消息一般是调用Handler实例的**sendMessage()方法,而发出的消息经过一系列辗转处理后,最终会交由handleMessage()**方法来处理
  • Handler是在主线程中创建的,handleMessage()方法也会在主线程中执行,故不存在UI操作引起的线程安全问题

MessageQueue

  • 消息队列,主要用于存放所有通过Handler发送的消息,这部分消息会一直存在于MessageQueue中,等待被处理;每个线程只有一个MessageQueue对象

Looper

  • Looper:每个线程中MessageQueue的管理者,调用loop()之类的方法后,就会进入到消息的循环监听中,每当发现MQ中存在消息,就会将其取出,传递到handler.handleMessage()方法中;每个线程中也只用一个Looper对象

异步处理流程

  • 1)在主线程中创建Handler对象,并重写handleMessage()方法
  • 2)当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将Message发送出去
  • 3)发送出的Message会被添加到MessageQueue中等待被处理
  • 4)Looper会一直监听MessageQueue中的消息,一旦发现待处理的消息就取出,再分发到Handler的handleMessage()方法中处理消息
    在这里插入图片描述
    如上,Message经过一系列辗转调用后,由子线程完成耗时操作的处理,再由主线程完成UI操作,通过消息的异步处理机制解决UI操作可能会导致的线程安全问题

其他异步处理的实现

  • 异步处理还可以通过定时任务来实现,一种是Java API提供的Timer类,一种Android的Alarm机制,这两种实现在多数情况下都能实现类似的效果,但Timer不太适用于长期在后台运行的定时任务
  • 为能让电池耐用,Android手机会在长时间不操作的情况下自动让CPU进入到睡眠状态,这可能会导致Timer中的定时任务无法正常运行;而Alarm有唤醒CPU功能,可保证在大多数情况下需要执行定时任务时CPU都能正常工作
    下面重点介绍下Alarm的基本使用:

Alarm机制

  • Android的Alarm机制是一种系统服务可在将来的某个时间点触发定时操作,即使你的应用程序不在运行;这个机制由AlarmManager类提供,它可以用于执行定时任务,比如在特定时间发送通知、启动服务或者执行其他后台操作

主要组件:

  • AlarmManager:系统服务,负责管理和触发闹钟,可通过getSystemService(Context.ALARM_SERVICE)获取实例
  • PendingIntent:描述将要执行操作的意图对象,当闹钟触发时,AlarmManager会发送对应的PendingIntent
  • AlarmManagerService:AlarmManager的服务端实现,运行在系统进程中,负责处理闹钟的触发

基本使用如下:

    public void sendAlarm(){
        Intent intent = new Intent(ALARM_ACTION);
        // 创建用于广播的延迟意图
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context,0,intent,PendingIntent.FLAG_IMMUTABLE);

        // 从系统服务中获取闹钟管理器
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        // 闹钟的触发时机
        long triggerTime = SystemClock.elapsedRealtime() + 2 * 1000;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // 允许在空闲时发送广播(Android6.0之后新增的方法)
            alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerTime, pendingIntent);
        } else {
            // 设置一次性闹钟,延迟若干秒后,携带延迟意图发送闹钟广播(Android6.0之后,set方法在暗屏时不保证发送广播)
            alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerTime,pendingIntent);
        }
        // 设置重复闹钟,每隔一定时间间隔就发送闹钟广播(从Android4.4开始,setRepeating方法不保证按时发送广播)
       //  alarmManager.setRepeating(AlarmManager.RTC_WAKEUP,System.currentTimeMillis(),1000,pendingIntent);

        // 取消闹钟
        // alarmManager.cancel(pendingIntent);

        // 获取下一个闹钟的信息
        // alarmManager.getNextAlarmClock();

    }
使用说明
  • AlarmManager部分源代码如下:
@SystemService(Context.ALARM_SERVICE)
public class AlarmManager {
    private static final String TAG = "AlarmManager";

   // ...
    /** @hide */
    @IntDef(prefix = { "RTC", "ELAPSED" }, value = {
            RTC_WAKEUP,
            RTC,
            ELAPSED_REALTIME_WAKEUP,
            ELAPSED_REALTIME,
    })
    // ...
    @Retention(RetentionPolicy.SOURCE)
    public @interface AlarmType {}
public void set(@AlarmType int type, long triggerAtMillis, @NonNull PendingIntent operation) {
    setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, operation, null, null,
                (Handler) null, null, null);
    }
    //  ...
}

参数说明:

  • type:指定AlarmManager的工作类型,有四个选项:RTC_WAKEUP、RTC、ELAPSED_REALTIME_WAKEUP、ELAPSED_REALTIME
    RTC_WAKEUP:让定时任务的触发时间从1970年1月1日0点开始算起,不会唤醒CPU;
    RTC:让定时任务的触发时间从1970年1月1日0点开始算起,不会唤醒CPU
    同理,
    ELAPSED_REALTIME_WAKEUP:让定时任务的触发时机从系统开机算起,会唤醒CPU;
    ELAPSED_REALTIME:让定时任务的触发时机从系统开机算起,但不会唤醒CPU
    =》带WAKEUP的会唤醒CPU,带ELAPSED_REALTIME的从系统开机算起
  • triggerAtMillis:定时任务触发的时间,单位为毫秒
    SystemClock.elapsedRealtime():从系统开机至今所经历的毫秒数
    System.currentTimeMillis():从1970年1月1日0点至今所经历的毫秒数
  • PendingIntent对象:一般调用getService()或getBroadcast()方法来获取执行服务或广播的PendingIntent;当定时任务被触发时,服务的onStartCommand()或广播接收器onReceive()方法就可得到执行

扩展

前面我们知道了如何异步地处理消息,实现原理,现在再来全面地看看消息异步处理解决的问题:

  • 1)线程安全:Android UI 是非线程安全的,即所有的 UI 操作必须在主线程中执行;任何在工作线程中直接对 UI 进行操作都会导致不可预知的行为,甚至可能导致应用崩溃;消息异步处理机制确保了所有的 UI 更新都在主线程中执行,从而保证了线程安全
  • 2)避免ANR(Application Not Responding):如果主线程因为长时间运行的任务(如数据库操作,执行复杂计算,网络请求)而被阻塞,系统会认为应用无响应,可能会触发ANR;消息异步处理机制允许这类耗时任务在工作线程中执行,从而避免了主线程的阻塞,减少ANR的发生
  • 3)控制线程的生命周期:使用Handler和Looper,开发者可精细地控制线程的生命周期(如在线程完成所有任务后退出,或在线程空闲时清理资源)
  • 4)支持延时消息和定时任务:Handler提供了发送延时消息的功能,允许在将来的某个时间点执行任务
  • 5)提高用户体验:通过在工作线程中执行耗时任务,用户界面可以保持响应,提供流畅的用户体验;用户可以继续与应用交互,而不会因为后台任务的执行而感到延迟

实现异步处理的方式:Handler(sendMessage和sendMessageDelayed方法)、Timer、Alarm机制、WorkManager、Coroutine,篇幅受限,这里不多讲解

参考资料:

  • 《Android第一行代码》第二版

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JDK17 你的下一个白月光

JDK版本升级的非常快&#xff0c;现在已经到JDK20了。JDK版本虽多&#xff0c;但应用最广泛的还得是JDK8&#xff0c;正所谓“他发任他发&#xff0c;我用Java8”。 但实际情况却不是这样&#xff0c;越来越多的java工程师拥抱 JDK17&#xff0c;于是了解了一下 JDK17新语法&a…

亲测几十款随身wifi,全网最全随身WiFi避坑指南!最值得买的随随身wifi品牌推荐!

关于随身wifi我认为我是比较有发言权的&#xff0c;历经三年测评了几十种随身wifi&#xff0c;便宜的贵的&#xff0c;大牌的小厂的&#xff0c;电池款USB款等各种随身wifi。根据测试结果以及通过电商平台搜索、粉丝反馈、社交平台评价等综合测评结果。今天就跟大家分享一下&am…

如何打造电力全域知识中心:知识库融合知识图谱

前言 随着人工智能技术的进步&#xff0c;智能化成为产业转型升级的关键抓手&#xff0c;国家电网在“十四五”发展规划中提出加快公司数字化转型进程、推进能源互联网企业建设的要求。知识管理能力建设作为强化企如何打造电力全域知识中心&#xff1a;知识库融合知识图谱业运…

5G消息 x 文旅 | 一站式智慧文旅解决方案

5G消息 x 文旅 | 一站式智慧文旅解决方案 文旅 x 5G 消息将进一步强化资源整合&#xff0c;满足游客服务需求、企业营销需求、政府管理需求&#xff0c;推进文化旅游项目的智慧化、数字化&#xff0c;增强传播力、竞争力和可持续性。5G 消息的“原生入口”、“超强呈现”、“智…

matlab 路面点云标线提取

目录 一、算法原理二、代码实现三、结果展示四、参考链接本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫与GPT。 一、算法原理 算法来自本人自创。实现效果如下图所示,具体实现原理看代码即可。 二、代码实现 clc; cle…

构建旧物回收系统的决策支持系统

内容概要&#xff1a; 在旧物回收系统中&#xff0c;构建一个有效的决策支持系统对于提高管理效率、优化资源配置具有重要意义。本文将探讨如何构建旧物回收系统的决策支持系统&#xff0c;并分析其如何辅助管理者做出更科学的决策。 一、决策支持系统的定义与功能 决策支持…

Opencv数一数有多少个水晶贴纸?

1.目标-数出有多少个贴纸 好久没更新博客了&#xff0c;最近家里小朋友在一张A3纸上贴了很多水晶贴纸&#xff0c;要让我帮他数有多少个&#xff0c;看上去有点多&#xff0c;贴的也比较随意&#xff0c;于是想着使用Opencv来识别一下有多少个。 原图如下&#xff1a; 代码…

校园车辆管理系统的设计与实现

第1章 绪论 1.1 研究背景与意义 随着高等教育的普及和扩张&#xff0c;大学校园已成为一个综合性的小型社会。教学楼、实验室、宿舍、体育设施等构成了庞大且复杂的校园基础设施。在这样的环境下&#xff0c;教师、学生、家长及访客的车辆数量也随之增多&#xff0c;这不仅带来…

python-02

面向对象 Python中把具有相同属性和方法的对象归为一个类。 class ClassName: 语句 class Myclass: # 定义类Myclassdef pp(self): # 定义方法pp()print("这是产品说明书")myclass Myclass() # 实例化类Myclass myclass.pp() # 调用Myclass中的方法pp()打印…

电脑缺失d3dcompiler_47.dll会怎么样,该如何修复呢

在计算机使用过程中&#xff0c;我们常常会遇到一些错误提示&#xff0c;其中之一就是“缺少d3dcompiler47.dll文件”。那么&#xff0c;d3dcompiler47.dll到底是什么&#xff1f;为什么计算机会缺失它&#xff1f;它会对电脑产生什么具体影响&#xff1f;如何解决这个问题&…

⭐Unity 控制任意UI的渐隐渐显

使用脚本之前先给要控制的UI加上CanvasGroup组件 解释: 这个脚本使用协程来逐渐改变CanvasGroup的alpha值&#xff0c;从而实现渐隐和渐显的效果。 Mathf.Lerp函数用于在指定的时间内平滑地从当前透明度过渡到目标透明度。 通过调用FadeIn和FadeOut方法&#xff0c;你可以在任…

SpringBoot 实现 阿里云语音通知(SingleCallByTts)

目录 一、准备工作1.开通 阿里云语音服务2.申请企业资质3.创建语音通知模板&#xff0c;审核通过4.调用API接口---SingleCallByTts5.调试API接口---SingleCallByTts 二、代码实现1.导入依赖 com.aliyun:aliyun-java-sdk-dyvmsapi:3.0.22.创建工具类&#xff0c;用于发送语音通知…

C++面向对象程序设计 - 函数库

C语言程序中各种功能基本上都是由函数来实现的&#xff0c;在C语言的发展过程中建立了功能丰富的函数库&#xff0c;C从C语言继承了些函数功能。如果要用函数库中的函数&#xff0c;就必须在程序文件中包含文件中有关的头文件&#xff0c;在不同的头文件中&#xff0c;包含了不…

RabbitMQ实践——交换器(Exchange)绑定交换器

在《RabbitMQ实践——交换器&#xff08;Exchange&#xff09;和绑定&#xff08;Banding&#xff09;》一文中&#xff0c;我们实验了各种交换器。我们可以把交换器看成消息发布的入口&#xff0c;而消息路由规则则是由“绑定关系”&#xff08;Banding&#xff09;来定义&…

GitLab教程(二):快速上手Git

文章目录 1.将远端代码克隆到本地2.修改本地代码并提交到远程仓库3.Git命令总结git clonegit statusgit addgit commitgit pushgit log 首先&#xff0c;我在Gitlab上创建了一个远程仓库&#xff0c;用于演示使用Gitlab进行版本管理的完整流程&#xff1a; 1.将远端代码克隆到本…

快速构建本地RAG聊天机器人:使用LangFlow和Ollama实现无代码开发

基于LangChain的快速RAG应用原型制作方法 还记得构建智能聊天机器人需要数月编码的日子吗&#xff1f; LangChain这样的框架确实简化了开发流程&#xff0c;但对非程序员来说&#xff0c;数百行代码仍然是一道门槛。 有没有更简单的方法呢&#xff1f; 图片由 Ravi Palwe 在…

数字政协:迈向智慧时代,开启政协工作新篇章

在信息化浪潮席卷全球的今天&#xff0c;数字技术不仅改变了我们的生活方式&#xff0c;也深刻影响着政治生态的变革。其中&#xff0c;“数字政协”的崛起&#xff0c;正是新时代政协工作创新发展的重要标志。那么&#xff0c;什么是数字政协&#xff1f;它又将如何助力政协工…

[图解]建模相关的基础知识-09

1 00:00:01,350 --> 00:00:03,780 首先&#xff0c;我们来看一下什么叫关系 2 00:00:05,370 --> 00:00:08,990 这个关系跟下面说的这些关系 3 00:00:09,000 --> 00:00:10,390 它不是一个东西 4 00:00:11,110 --> 00:00:14,950 比如说&#xff0c;我们UML类图上&…

门控循环单元GRU与长短期记忆网络LSTM

门控循环单元与长短期记忆网络 门控隐状态 问题提出&#xff1a;对于一个序列来说不是每个观察值都是同等重要想只记住相关的观察需要&#xff1a; 能关注的机制&#xff08;更新门&#xff09;能遗忘的机制&#xff08;重置门&#xff09; 第一个词元的影响至关重要。 我们…

使用vuejs3时,报错:Uncaught (in promise)

解决&#xff1a; vite.config.js里 import {fileURLToPath, URL} from node:urlimport {defineConfig} from vite import vue from vitejs/plugin-vue// https://vitejs.dev/config/ export default defineConfig({resolve: {alias: {: fileURLToPath(new URL(./src, import…