工作填坑 - Android 高版本中 AlarmManager , PendingIntent 的坑
前提
这两天要做一个需求,简单来讲,就是定时的往服务器上传数据,原本以为是一个很简单的需求,选择合适的定时器,然后传递数据,执行网络操作,结果发现遇到了各种坑
定时器选择
Android 上能做定时器的有好几种方式,大概有以下几种,
- Timer
- AlarmManager
- Handler
- Thread
这几种方式的优劣可以参考 Android实现定时器的几种方法 里面写的很详细。
最终我选择了 AlarmManager 方式,原因不解释
AlarmManager
使用方式,这个就简单了。可是谁知道,越是简单的东西,往往坑越多,先看我们通用的实现定时器的方式吧。
1. 得到 AlarmManager 对象
这个简单,
AlarmManager mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
2. 选择要开启的组件
是一个 Activity ,还是 Service ,或者 Broadcast , 因为要执行网络操作,耗时的,肯定在子线程中,那最明智的选择就是IntentService 因为这个 IntentService 要处理相应的上传操作,我又不想创建新的网络操作对象,就直接写了一个内部类,结果一个坑就在这里了,后面会将解释
public class TimerActivity extends AppCompatActivity {
private static final String TAG = "hoyouly";
private RetrofitUtils mRetrofitUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public class UploadService extends IntentService {
public UploadService() {
super("UploadService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
Log.d(TAG, "onHandleIntent: ");
}
}
}
然后通过 Intent 启动这个Service
mIntent = new Intent(context, TimerActivity.UploadService.class);
3. 创建相应的PendingIntent
这个说是简单,主要是这几个参数的意思。
mPendingIntent = PendingIntent.getService(context,
0 , mIntent , PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent 的 补充
- 典型的使用场景就是给 RemoteView 添加点击事件。
- 通过send /cancel 发送或者取消特定的Intent.
- PendingIntent 和 Intent 的区别
- PendingIntent :将来的某个不确定时间发生
- Intent: 立刻发生
- PendingIntent的 匹配规则
- PendingIntent 的 匹配规则: 如果两个 PendingIntent 内部的 Intent 相同并且 requestCode 也相同,那么这两个 PendingIntent 就是相同。
- Intent 相同的规则:如果两个 Intent 的 ComponetName 和intent-filter 都相同,那么这两个 Intent 就相同。 注意: Extras不参与 Intent 的匹配过程。只要 Intent 的 ComponetName 和intent-filter 相同,即使他们的 Extras 不相同,那么这两个 Intent 也会匹配成功的。
- PendingIntent支持三种特定的意图,启动 Activity ,启动 Service ,发送 Broadcast 。如下图所示
- 参数中的 requestCode 和 flags 的意思
- requestCode PendingIntent 发送方的请求码,多数情况下为 0 ,会影响到 flags 的效果
- flags 常见的类型有四种
- FLAG_ONE_SHOT
当前描述的 PendingIntent 只能被使用一次,然后就会被自动 cancel 。如果后续还有相同的 PendingIntent ,那么后续的 send 方法就会发送失败。对于通知栏来说,采用这种个标记,那么同类通知只能出现一次,后续的通知单击后将无法打开 - FLAG_NO_CREATE
当前描述的 PendingIntent 不会主动创建,如果当前的 PendingIntent 不存在,那么 getActivity() , getService() , getBroadcast() 就会返回 null ,即获取 PendingIntent 失败,这个很少使用。 - FLAG_CANCEL_CURRENT
如果当前描述的 PendingIntent 已经存在,那么他们都会被取消,然后系统会自动创建一个新的 PendingIntent ,对于通知栏来说,那些被 cancel 的消息将无法打开。后续的通知单击后将无法打开 - FLAG_UPDATE_CURRENT
当前描述的 PendingIntent 如果已经存在,那么他们都会被更新,即他们的 intent 中的 Extras 会被更新。
- FLAG_ONE_SHOT
4. 设置定时器
好久不用 mAlarmManager 了,第一次直接使用的 set 方法,后来发现这个只能使用一次,不能重复,要使用 setRepeating() 方法才行。坑又来了,后面会说道这个坑。
mAlarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(), TIME_INTERVAL , mPendingIntent);
思路理清了,开始写代码吧。
写代码
- 我把这一部分都封装起来,成了一个 AlarmManagerWrapper 类
public class AlarmManagerWrapper {
private static AlarmManagerWrapper mInstance;
//循环上报的间隔,先定 10 秒,方便自测。
private static final long TIME_INTERVAL = 10 * 1000;
private PendingIntent mPendingIntent;
private AlarmManager mAlarmManager;
private final Intent mIntent;
private AlarmManagerWrapper(Context context) {
mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mIntent = new Intent(context, TimerActivity.UploadService.class);
mPendingIntent = PendingIntent.getService(context,
0 , mIntent , PendingIntent.FLAG_UPDATE_CURRENT);
}
public static AlarmManagerWrapper getInstance(Context context) {
if (mInstance == null) {
synchronized (AlarmManagerWrapper.class) {
if (mInstance == null) {
mInstance = new AlarmManagerWrapper(context);
}
}
}
return mInstance;
}
/**
* 取消定时
*/
public void cancelAlarm() {
mAlarmManager.cancel(mPendingIntent);
}
/**
* 开启定时
*/
public void startAlarm() {
mAlarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime(), TIME_INTERVAL , mPendingIntent);
}
}
- 在 Activity 中设置了两个点击事件,开始和结束
public class TimerActivity extends AppCompatActivity {
private static final String TAG = "hoyouly";
private AlarmManagerWrapper mWrapper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mNetWork = NetWork.getInstance(this);
mWrapper = AlarmManagerWrapper.getInstance(this);
}
public void cancel(View view) {
mWrapper.cancelAlarm();
}
public void start(View view) {
mWrapper.startAlarm();
}
public class UploadService extends IntentService {
public UploadService() {
super("UploadService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
Log.d(TAG, "onHandleIntent: ");
//TODO 上传数据
}
}
}
- 在AndroidManifest.xml中注册该 Service ,这点不能忘记。
<service android:name=".TimerActivity$UploadService"/>
满心欢喜的以为这样就行了,可是谁知道,运行后,点击开始按钮,然后崩了,瞬间心情就不好了,但是没办法,咱的工作就是写 bug 然后改 bug 的啊。好吧
adb logcat -b crash
查看崩溃日志吧,然后就看到了
java.lang.InstantiationException:
java.lang.Class<***.TimerActivity$UploadService> has no zero argument constructor
第一个坑出现了。
坑一:内部类的组件必须得是static
这是毛线啊,我明明设置无参构造函数了啊。不懂简单, Google 吧。于是就有了大神的解释,简单的来说就是内部 Service 需要设置成静态的,原因参考大神的解释 No empty constructor when create a service。
好吧,那就设置成静态的吧,其实我是不太想设置成静态的,因为如果设置成静态的,那么我前面的请求网络的变量也的设置成静态的,可是这样好像不太好,但是又没有其他的好处理的办法。
注:没有直接在这个内部类中创建一个新的网络请求参数变量,是因为我们业务逻辑中,这个网络请求变量创建需要很多参数,太麻烦,就想直接使用外部类中的那个变量。
改好后的代码就如下了
public class TimerActivity extends AppCompatActivity {
private static final String TAG = "hoyouly";
private static NetWork mNetWork;
private AlarmManagerWrapper mWrapper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mNetWork = NetWork.getInstance(this);
mWrapper = AlarmManagerWrapper.getInstance(this);
}
public void cancel(View view) {
mWrapper.cancelAlarm();
}
public void start(View view) {
mWrapper.startAlarm();
}
public static class UploadService extends IntentService {
public UploadService() {
super("UploadService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
Log.d(TAG, "onHandleIntent: ");
//TODO 上传数据
}
}
}
注意内部类 加上了 static 关键字,
log 是打印出来了,
11-07 18:54:02.892 13581 13613 D hoyouly : onHandleIntent:
11-07 18:55:32.467 13581 13660 D hoyouly : onHandleIntent:
11-07 18:56:04.088 13581 13678 D hoyouly : onHandleIntent:
但是感觉不对啊,我设置的 10 秒请求,可是这间隔也太长了吧,一分钟半才执行一次,这是个毛线问题啊。第二个坑来了
坑二:Android 6.0 为了性能优化修改 AlarmManager 的定时API
继续 Google 吧。原来在Android 6.0 后, Google 为了 对低电耗模式和应用待机模式进行针对性优化,改 API 了,需要使用 setExactAndAllowWhileIdle() 这个 API 定时发送才行,具体原因查看 关于使用 AlarmManager 的注意事项。
按照这上面的重新修改后的代码如下, AlarmManagerWrapper 和 UploadService 都需要改
public class AlarmManagerWrapper {
private static AlarmManagerWrapper mInstance;
//循环上报的间隔,先定 10 秒,方便自测。
private static final long TIME_INTERVAL = 10 * 1000;
private PendingIntent mPendingIntent;
private AlarmManager mAlarmManager;
private final Intent mIntent;
private AlarmManagerWrapper(Context context) {
mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mIntent = new Intent(context, TimerActivity.UploadService.class);
mPendingIntent = PendingIntent.getService(context,
0 , mIntent , PendingIntent.FLAG_UPDATE_CURRENT);
}
public static AlarmManagerWrapper getInstance(Context context) {
if (mInstance == null) {
synchronized (AlarmManagerWrapper.class) {
if (mInstance == null) {
mInstance = new AlarmManagerWrapper(context);
}
}
}
return mInstance;
}
/**
* 取消定时
*/
public void cancelAlarm() {
mAlarmManager.cancel(mPendingIntent);
}
/**
* 开启定时
*/
public void startAlarm() {
mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP
, SystemClock.elapsedRealtime(), mPendingIntent);
}
/**
* 由于Android 6.0 对低电耗模式和应用待机模式进行针对性优,
* 所以需要再接受到执行定时任务的时候,再次开启,保证在低电耗模式下的也能正常执行
*/
public void startAgain() {
mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP
, SystemClock.elapsedRealtime() + TIME_INTERVAL, mPendingIntent);
}
}
public static class UploadService extends IntentService {
public UploadService() {
super("UploadService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
Log.d(TAG, "onHandleIntent: ");
AlarmManagerWrapper.getInstance(getApplicationContext()).startAgain();
//TODO 上传数据
}
}
这样 log 看起来就正常了,每 10 秒打印一次。
11-07 19:02:36.930 14109 14138 D hoyouly : onHandleIntent:
11-07 19:02:46.938 14109 14145 D hoyouly : onHandleIntent:
11-07 19:02:56.945 14109 14154 D hoyouly : onHandleIntent:
11-07 19:03:06.955 14109 14161 D hoyouly : onHandleIntent:
11-07 19:03:16.967 14109 14167 D hoyouly : onHandleIntent:
11-07 19:03:26.975 14109 14172 D hoyouly : onHandleIntent:
可我还想再每次上传数据的时候传递一些参数过去啊,
通过 PendingIntent 传递参数
这个应该简单了吧,通过 Intent ,然后 putExtra() ,不管是基本数据类型,还是 Parcelable 类型的, Serializable 类型的,都可以,因为业务需要传递一个 Parcelable 类型实体对象比较合适。那就传递吧,简单。
- 在 startAlarm() 中接受一个实现 Parcelable 类型的对象(Trip.java),然后放到 Intent 中
public void startAlarm(Trip data) {
mIntent = new Intent(mContext, TimerActivity.UploadService.class);
mIntent.putExtra("trip",data);
mPendingIntent = PendingIntent.getService(mContext, 0 , mIntent
, PendingIntent.FLAG_UPDATE_CURRENT);
mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP
, SystemClock.elapsedRealtime(), mPendingIntent);
}
- 在调用该方法的地方创建一个 Trip 对象,传递过去
public void start(View view) {
Trip trip = new Trip(1, "hello");
mWrapper.startAlarm(trip);
}
- 在 UpdateService 中接受这个对象
@Override
protected void onHandleIntent(@Nullable Intent intent) {
Trip trip = intent.getParcelableExtra("trip");
Log.d(TAG, "onHandleIntent: " + trip);
AlarmManagerWrapper.getInstance(getApplicationContextstartAgain().startAgain();
//TODO 上传数据
}
- 打印这个对象,打印结果如下:
11-07 22:44:07.974 8596 8664 D hoyouly : onHandleIntent: null
11-07 22:44:17.980 8596 8668 D hoyouly : onHandleIntent: null
11-07 22:44:27.985 8596 8675 D hoyouly : onHandleIntent: null
11-07 22:44:37.993 8596 8681 D hoyouly : onHandleIntent: null
11-07 22:44:47.997 8596 8683 D hoyouly : onHandleIntent: null
11-07 22:44:58.002 8596 8686 D hoyouly : onHandleIntent: null
11-07 22:45:08.009 8596 8688 D hoyouly : onHandleIntent: null
11-07 22:45:18.014 8596 8692 D hoyouly : onHandleIntent: null
11-07 22:45:28.019 8596 8693 D hoyouly : onHandleIntent: null
11-07 22:45:38.025 8596 8697 D hoyouly : onHandleIntent: null
11-07 22:45:48.031 8596 8698 D hoyouly : onHandleIntent: null
11-07 22:45:58.036 8596 8710 D hoyouly : onHandleIntent: null
11-07 22:46:08.040 8596 8712 D hoyouly : onHandleIntent: null
11-07 22:46:18.045 8596 8720 D hoyouly : onHandleIntent: null
11-07 22:46:28.056 8596 8723 D hoyouly : onHandleIntent: null
怎么一直是 null 呢,不应该啊,断点调试,发现在创建 PendingIntent 的时候,那个 mIntent 对象里面是有这个 trip 的啊,第三个坑出现了。
坑三:Android 7.0 后通过 PendingIntent 传递 Parcelable 类型数据为null
继续 Google , PendingIntent 参数为 null ,网上说了一大堆,和什么创建 PendingIntent 的时候的 requestCode 或者 flags 有关,可是按照他们说的做了,还是为 null 。
最后无意间发现了,原来是一个 bug ,可以参照 Android 7.0 pendingIntent bug(AlarmManager通过 PendingIntent 传递数据(跨进程数据传递) 上面的解释,按照上面的方法做,不传递 Parcelable 类型的对象,而是把对象转换成 String 。
public void startAlarm(String data) {
mIntent = new Intent(mContext, TimerActivity.UploadService.class);
mIntent.putExtra("trip",data);
mPendingIntent = PendingIntent.getService(mContext, 0 , mIntent
, PendingIntent.FLAG_UPDATE_CURRENT);
mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP
, SystemClock.elapsedRealtime(), mPendingIntent);
}
再传递,结果正常了
11-07 22:55:18.480 9151 9184 D hoyouly : onHandleIntent: {"time":1,"name":"hello"}
11-07 22:55:28.485 9151 9186 D hoyouly : onHandleIntent: {"time":1,"name":"hello"}
11-07 22:55:38.491 9151 9189 D hoyouly : onHandleIntent: {"time":1,"name":"hello"}
11-07 22:55:48.494 9151 9192 D hoyouly : onHandleIntent: {"time":1,"name":"hello"}
11-07 22:55:58.498 9151 9198 D hoyouly : onHandleIntent: {"time":1,"name":"hello"}
11-07 22:56:08.502 9151 9200 D hoyouly : onHandleIntent: {"time":1,"name":"hello"}
11-07 22:56:18.507 9151 9203 D hoyouly : onHandleIntent: {"time":1,"name":"hello"}
11-07 22:56:28.524 9151 9205 D hoyouly : onHandleIntent: {"time":1,"name":"hello"}
目前为止,坑终于平晚了,主要还是 Android 新版本的一些问题,原本熟悉的正常的代码到了新版本,尤其是Android 6.0 以后,就会遇到各种坑。不过总算解决了,唯一美中不足的是,要把 Service 变成静态内部类,这样他使用的所有外部类的变量都得成静态的,可是我又没办法把 Service 变成单独的类,如果这样,需要创建的变量会更麻烦,所以只能这样了。
搬运地址:
No empty constructor when create a service
Android 7.0 pendingIntent bug(AlarmManager通过 PendingIntent 传递数据(跨进程数据传递
既已览卷至此,何不品评一二: