作者@恺风Wei。
IntentService的问题
IntentService确实很不错,因为是service,可以确保进程运行,同时,不在主线程中执行,避免ANR消息。但是IntentService在广播接收器中使用会有一些问题。当一个广播接收器被触发时,特别是通过告警管理器进行触发,如果设备休眠,alarm管理器会通过一个wake lock(唤醒锁)通知电源管理器开启设备(只需能够运行代码,不需要UI呈现)。wake lock会在broadcast receiver返回时释放。然而IntentService的触发没有wake lock,设备可能在代码运行前就sleep了。
因此,我们需要在在线程开启前获取wake lock,并在处理结束时释放。为了在主线程和在worker线程中分别对同一wake lock的获取和释放,采用static的方式。在具体开始小例子之前,我们先看看wake lock。
长时间处理通知的小例子
利用IntentService实现长时间的通知处理,要求从收到通知开始,到通过IntentService,在线程完成通知的处理的整个过程中要持有wake lock,确保有资源执行任务,并在通知处理完后释放资源。小例子将提供一个通用的解决方法,较之以往的小例子,本例稍微复杂一点,但是了解小例子的设计,小例子不过就是个小例子。
对于一个通用的小例子,在context中如何持有和释放wake lock,需要考虑的是在上一次通知没有处理完,又收到新的通知。context中可能有多个接收器,触发不同的IntentService。这些情况都需要得到很好的处理。下图是整个小例子的框架流程。
在小例子中,开灯表示持有wake lock,关灯表示释放wake lock。房间表示线程处理,可能是在工作线程A,也可能在工作线程B。有个计数器,用于记录在线程中处理或者等待处理的的通知的总数。通知在onHandleIntent()中进行处理,处理完时,要离开房间,这时要检查一下是否所有的通知已全部处理(计数器为零),如果没有通知就要关灯。
此外通过stopService()可以强制终止service,强制清空了某个工作线程所要处理的全部通知,又或者由于onHandleIntent()的处理过程中出现异常退出,没能运行leaveRoom()。这种情况,计数器无法正确反映正在处理或等待处理的通知数。因此我们通过在onCreate()的registerClient()和在onDestroy()的unregisterClient(),记录当前运行的IntentService数目(房间数),如果发现房间数为零,也说明已经没有通知要处理。(要注意,stopService()会调用onDestroy(),当并不能终止后台运行的线程,后台线程将会继续执行,代码中可以通过设置状态检查等方式来终止后台线程)
wake lock的代码实现
使用wakelock需要WAKE_LOCK的权限,我们将上面的过程形象地模拟为:开启一个IntentService,如同客人注册,开一间房,当IntentService结束时,客人注销,退房。收到一个通知,表示一个人进入某一间房,处理完一个通知,表示某个人离开某间房。我们将统计在房间中的总人数,如果发现没有人,将关灯。由于某种情况,人离开房间可能没有被观察到,所以仅通过总人数判断不够。当发现所有的房间都退掉时,表示房间都没有人,要光灯,并复位房间的总人数为零。
下面通过LightedGreenRoom这个类来管理client人数、房间计数器、开灯和光灯的操作,相关处理代码如下:
public class LightedGreenRoom {
private static String tag="LightedGreenRoom";
private int count; //记录当前有多少通知在工作线程中处理或等待处理
private Context ctx = null;
PowerManager.WakeLock wl = null;
private int clientCount = 0; //记录工作线程(客人)数目
public LightedGreenRoom(Context inCtx){
ctx = inCtx;
wl = this.createWakeLock(inCtx); //基于context创建wake lock }
//有一个通知要处理:模拟为进入房间,房间内人数++
public synchronized int enter(){
count ++;
Log.d(tag,"A new visitor , count = " + count);
return count;
}
//有一个通知处理结束:模拟为离开房间,房间内人数--
public synchronized int leave(){
if(count == 0){
Log.d(tag,"No one in room.");
return count;
}
count --;
Log.d(tag,"one leave, count = " + count);
if(count == 0){
turnOffLights();
}
return count;
}
// 获取当前要处理的通知数:模拟为有多少人在房间?
synchronized public int getCount(){
return count;
}
//开灯
private void turnOnLights(){
Log.d(tag,"Turning on lights. Count = " + count);
this.wl.acquire();//wake lock操作(3):请求持有wake lock }
//关灯
private void turnOffLights(){
if(this.wl.isHeld()){
Log.d(tag,"Turning off ligths. Count = " + count);
this.wl.release(); //wake lock操作(4):释放wake lock }
}
//对于wake lock的操作:(1)获得电源管理器参考;(2)获得wake lock对象,PARTIAL_WAKE_LOCK表示无需屏幕亮,维持最小的运行需求
private PowerManager.WakeLock createWakeLock(Context inCtx){
PowerManager pm = (PowerManager) inCtx.getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag);
return wl;
}
//IntentService的开启:模拟为客人注册,开间房
private int registerClient(){
Utils.logThreadSignature(tag);
this.clientCount ++;
Log.d(tag,"register a new Client ,client count = " + clientCount);
return clientCount ;
}
//IntentService的结束:模拟为客人注销,房间退
private int unregisterClient(){
Utils.logThreadSignature(tag);
if(clientCount == 0){
Log.d(tag,"no client registed!");
return 0;
}
clientCount --;
Log.d(tag,"unregister a client , client count = " + clientCount);
if(clientCount == 0){
emptyTheRoom();
}
return clientCount;
}
//清空房间
synchronized public void emptyTheRoom(){
Log.d(tag,"Empty the Room ");
count = 0;
this.turnOffLights();
}
}
在IntentService中的应用
从流程框架图中,我们观察到LightedGreenRoom需要在接收器、IntentService的的主线程已经工作线程中调用。接收器开启IntentService,这是通过intent进行数据传递,是没有办法传递对象的参考。因此,我们需要一个全部的LightedGreenRoom对象,要么采用static的方式,要么作为一个application的变量。作为通用范例,能更好地在不同app中使用,我们采用了static方式。为此,对LightedGreenRoom进行了补充:
public class LightedGreenRoom {
... ...
private static LightedGreenRoom s_self = null; //维持一个全局的LightedGreenRoom对象
//第一次收到通知,生成全局对象,并开灯(在具体处理的前期准备也要开灯,只要在处理,都要开灯) public static void setup(Context inCtx){
if(s_self == null){
Log.d(tag,"Create Lighted Green Room.");
s_self = new LightedGreenRoom(inCtx);
s_self.turnOnLights();
}
}
public static boolean isSetUp(){
return (s_self == null ? false: true);
}
//收到通知,模拟有人进入房间
public static int s_enter(){
assertSetup();
return s_self.enter();
}
//通知处理完:模拟有人离开房间
public static int s_leave(){
assertSetup();
return s_self.leave();
}
//清空房间
public static void ds_emptyTheRoom(){
assertSetup();
s_self.emptyTheRoom();
}
//开启IntentService,模拟开房
public static void s_registerClient(){
assertSetup();
s_self.registerClient();
}
//结束IntentService,模拟退房
public static void s_unregisterClient(){
assertSetup();
s_self.unregisterClient();
}
//检查是否已经正确初始化
private static void assertSetup(){
if(!LightedGreenRoom.isSetUp()){
Log.w(LightedGreenRoom.tag,"You need to call setup first.");
throw new RuntimeException("You need to setup GreenRoom first");
}
}
}
小例子代码在:Pro Android学习:Broadcast小例子