基本介绍
- 当我们需要执行复杂的计算逻辑,网络请求等耗时操作时,服务器可能不会立即响应请求,如果不将这类操作放在子线程中运行,就会导致主线程被阻塞住,从而影响用户的使用体验
- 如果想要更新应用程序中的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第一行代码》第二版