原文链接https://www.shanya.world/archives/a7b639d4.html
创建定时任务
Android中的定时任务一般有两种实现方式,一种是使用 Java API 里提供的 Timer 类,一种是使用 Android 的 Alarm 机制。这两种方式在多数情况下都能实现类似的效果,但 Timer 有一个明显的短板,它并不适用于那些需要长期在后台运行的定时任务。我们都知道,为了能让电池更加耐用,每种手机都会有自己的休眠策略,Android 手机就会在长时间不操作的情况下自动让 CPU 进入到睡眠状态,这就有可能导致 Timer 中的定时任务无法正常运行。而 Alarm 则具有唤醒 CPU 的功能,它可以在需要执行定时任务的时候大吼一声:“小UU,不要跟我 bbll ,赶紧给我起来干活,不然你看我扎不扎你就完了。”
需要注意,这里唤醒 CPU 和唤醒屏幕完全不是一个概念,千万不要混淆。
Alarm机制
首先看一下 Alarm 机制的用法,并不复杂,主要就是借助了 AlarmManager 类实现的。
获取 AlarmManager 的实例,代码如下:
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
API19之前AlarmManager常用的一些方法
-
set(int type,long startTime,PendingIntent pi)
//该方法用于设置一次性定时器,到达时间执行完就GG了 -
setRepeating(int type,long startTime,long intervalTime,PendingIntent pi)
//该方法用于设置可重复执行的定时器 -
setInexactRepeating(int type,long startTime,long intervalTime,PendingIntent pi)
//该方法用于设置可重复执行的定时器。与setRepeating相比,这个方法更加考虑系统电量,比如系统在低电量情况下可能不会严格按照设定的间隔时间执行闹钟,因为系统可以调整报警的交付时间,使其同时触发,避免超过必要的唤醒设备。
参数说明
int type:闹钟类型,常用有几个类型,说明如下:
AlarmManager.ELAPSED_REALTIME | 表示闹钟在手机睡眠状态下不可用,就是睡眠状态下不具备唤醒CPU的能力(跟普通Timer差不多了),该状态下闹钟使用相对时间,相对于系统启动开始。 |
AlarmManager.ELAPSED_REALTIME_WAKEUP | 表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟也使用相对时间 |
AlarmManager.RTC | 表示闹钟在睡眠状态下不可用,该状态下闹钟使用绝对时间,即当前系统时间 |
AlarmManager.RTC_WAKEUP | 表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟使用绝对时间 |
long startTime: 定时任务的出发时间,以毫秒为单位。
PendingIntent pi: 到时间后的执行意图。PendingIntent是Intent的封装类。需要注意的是,如果是通过启动服务来实现闹钟提 示的话,PendingIntent对象的获取就应该采用Pending.getService(Context c,int i,Intent intent,int j)方法;如果是通过广播来实现闹钟提示的话,PendingIntent对象的获取就应该采用 PendingIntent.getBroadcast(Context c,int i,Intent intent,int j)方法;如果是采用Activity的方式来实现闹钟提示的话,PendingIntent对象的获取就应该采用 PendingIntent.getActivity(Context c,int i,Intent intent,int j)方法。关于PendingInten不是本文重点,请自行查阅使用方法。
使用举例
需求:定义一个在CPU休眠情况下也能执行的闹钟,到==指定时间==发送一次广播,代码如下:
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY,21);
calendar.set(Calendar.MINUTE,14);
calendar.set(Calendar.SECOND,00);//这里代表 21.14.00
Intent intent = new Intent("Li_Hua");
intent.putExtra("msg","起床了啊");
PendingIntent pi = PendingIntent.getBroadcast(this,0,intent,0);
// 到了 21点14分00秒 后通过PendingIntent pi对象发送广播
am.set(AlarmManager.RTC_WAKEUP,calendar.getTimeInMillis(),pi);
AlarmManager的版本适配
以上的讲解和代码在 API < 19 的情况下是可以正常运行的。但是在 API > 19 的手机上运行就会发现 TMD 怎么就不好使了呢。例如我们设置1分钟后执行,结果却是2分钟后才执行。
But 这不是 BUG!
原因是 Google 对系统耗电性方面进行了优化。系统会自动检测目前有多少 Alarm 任务存在,然后将触发时间相近的几个任务放在一起执行,这就可以大幅度减少 CPU 被唤醒的次数,从而有效延长电池的使用时间。
当然,如果你要求的 Alarm 任务执行时间必须准确无误,Android 仍然提供了解决方案。使用 AlarmManager 的 setExact()
方法来代替set()
方法,就基本上可以保证任务能够准时执行了。
虽然Android的每个系统版本都在手机电量方面努力进行优化,不过一直没能解决后台服务泛滥、手机电量消耗过快的问题。于是在Android 6.0系统中,谷歌加入了一个全新的Doze模式,从而可以极大幅度地延长电池的使用寿命。
到底什么是Doze模式。当用户的设备是Android 6.0或以上系统时,如果该设备未插接电源,处于静止状态( Android 7.0中删除了这条件), 且屏幕关闭了一段时间之后,就会进人到Doze模式。在Doze模式下,系统会对CPU、网络、Alarm等活动进行限制,从而延长了电池的使用寿命。
当然,系统并不会一直处于 Doze模式,而是会间歇性地退出Doze模式一小段时间,在这段时间中,应用就可以去完成它们的同步操作、Alarm任务, 等等。
接下来我们具体看- 看在Doze模式下有哪些功能会受到限制吧。
- 网络访问被禁止
- 系统忽略唤醒CPU或者屏幕操作
- 系统不再执行WIFI扫描
- 系统不再执行同步服务
- Alarm任务将会在下次退出Doze模式的时候执行
注意其中的最后一条, 也就是说,在Doze模式下,我们的Alarm任务将会变得不准时。当然,这在大多数情况下都是合理的,因为只有当用户长时间不使用手机的时候才会进入Doze模式,通常在这种情况下对Alarm任务的准时性要求并没有那么高。
不过,如果你真的有非常特殊的需求,要求Alarm任务即使在Doze模式下也必须正常执行,Android还是提供了解决方案。调用AlarmManager的setAndAllowWhileIdle()
或setExact-AndAllowhileIdle()
方法就能让定时任务即使在Doze模式下也能正常执行了,这两个方法之间的区别和set()
、setExact()
方法之间的区别是一样的。
AlarmManager实例Demo讲解(包含版本适配以及高版本设置重复闹钟)
好了经过上面讲解,我相信你是似懂非懂的,因为没看到具体代码啊,简单,一个小Demo就全都明白了。
实现功能:在CPU休眠情况下依然可以设定时间启动一次服务,在服务中执行相应逻辑(Demo中只是打印Log),适配各个版本。
先看一下最核心的AlarmManagerUtils类(AlarmManager工具类):
package com.shanya.testalarm;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import java.util.Calendar;
public class AlarmManagerUtils {
private static final long TIME_INTERVAL = 10 * 1000;//闹钟执行任务的时间间隔
private Context context;
public static AlarmManager am;
public static PendingIntent pendingIntent;
private Calendar calendar;
//
private AlarmManagerUtils(Context aContext) {
this.context = aContext;
}
//singleton
private static AlarmManagerUtils instance = null;
public static AlarmManagerUtils getInstance(Context aContext) {
if (instance == null) {
synchronized (AlarmManagerUtils.class) {
if (instance == null) {
instance = new AlarmManagerUtils(aContext);
}
}
}
return instance;
}
public void createGetUpAlarmManager() {
am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, MyService.class);
pendingIntent = PendingIntent.getService(context, 0, intent, 0);//每隔10秒启动一次服务
}
@SuppressLint("NewApi")
public void getUpAlarmManagerStartWork() {
calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY,23);
calendar.set(Calendar.MINUTE,50);
calendar.set(Calendar.SECOND,00);
//版本适配 System.currentTimeMillis()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
calendar.getTimeInMillis(), pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
am.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
pendingIntent);
} else {
am.setRepeating(AlarmManager.RTC_WAKEUP,
calendar.getTimeInMillis(), TIME_INTERVAL, pendingIntent);
}
}
@SuppressLint("NewApi")
public void getUpAlarmManagerWorkOnOthers() {
//高版本重复设置闹钟达到低版本中setRepeating相同效果
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + TIME_INTERVAL, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()
+ TIME_INTERVAL, pendingIntent);
}
}
}
AlarmManagerUtils就是将与AlarmManager有关的操作都封装起来了,方便解耦。很简单,主要就是版本适配了,上面已经讲解够仔细了,这里就是判断不同版本调用不同API了。
MainActivity代码:
package com.shanya.testalarm;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private AlarmManagerUtils alarmManagerUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
alarmManagerUtils = AlarmManagerUtils.getInstance(this);
alarmManagerUtils.createGetUpAlarmManager();
Button button = findViewById(R.id.am);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
alarmManagerUtils.getUpAlarmManagerStartWork();
Toast.makeText(getApplicationContext(),"设置成功",Toast.LENGTH_SHORT).show();
}
});
}
}
MainActivity中就是调用AlarmManagerUtils中已经封装好的代码进行初始化以及点击Button的时候调用getUpAlarmManagerStartWork方法完成第一次触发AlarmManager。
最后看下服务类中具体做了什么。
MyService类:
package com.shanya.testalarm;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
public class MyService extends Service {
private static final String TAG = "MyService";
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "run: ");
}
}).start();
AlarmManagerUtils.getInstance(getApplicationContext()).getUpAlarmManagerWorkOnOthers();
return super.onStartCommand(intent, flags, startId);
}
}
总结
好了,本文到此就该结束了,相信经过以上讲述你对AlarmManager有了更进一步全面了解,在我们使用的时候请不要滥用,考虑一下用户电量,尽量优化自己APP。