作者@恺风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。这些情况都需要得到很好的处理。下图是整个小例子的框架流程。

Pro Android学习笔记(一零一):BroadcastReceiver(5):长时间处理通知小例子(上)_Android

在小例子中,开灯表示持有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小例子