5.3.2 服务组件的生命周期
服务组件的使用方式可分为两种,分别是调用服务和绑定服务。在不同的使用方式下,服务组件的生命周期模型既有相同之处,也有不同之处。
如图5-10所示,不论在何种使用模式下,组件的生命周期都是从Service.onCreate函数开始,至Service.onDestroy函数结束。因此,在服务组件的开发中,开发者可以选择在Service.onCreate函数中做数据加载等初始化工作,而在Service.onDestroy函数中做数据销毁、线程终止等清理工作。
图 5-10 界面组件的状态保存
在绑定模式下,当Service.onBind函数被调用时,说明服务组件已被前台界面组件绑定,服务组件应该根据调用者传递的Intent对象,在该函数内加载资源,构建通信对象,等待绑定者的调用。当界面组件完成相关操作,会解除与服务组件的绑定,此时,服务组件的Service.onUnbind函数会被调用,可以在该函数内做一些统计或资源清理工作。
被绑定服务组件的进程状态,与绑定该服务的界面组件密切相关。如果其绑定组件为前台界面组件,那么服务所处的进程即为前台进程;如果绑定该服务的界面组件是一个可视界面组件,那么服务所在的进程则为可视进程。
Android系统不会轻易回收前台进程或可视进程,因此,处于绑定状态的服务组件通常也不会被强制停止。对于开发者而言,绑定服务后一定不要忘记选择合适的时机解除绑定,否则将使服务组件停留在前台或可视状态无法回收,从而浪费系统资源。
在调用模式下,当服务组件执行Service.onStartCommand函数时[1],服务所在的进程为前台进程,拥有最高的优先级。当Service.onStartCommand函数执行完成后,如果没有显性调用Service.stopSelf等相关函数来停止服务,那么服务组件将会成为后台组件继续提供服务,直至调用Service.stopSelf函数显性停止,或者被系统强行回收。
当系统资源紧张时,后台服务组件会被系统强行终止回收,而此时,服务组件进行的操作可能尚未完成。从Android 2.0开始,系统提供了Service.onStartCommand函数,取代原有的Service.onStart函数。onStartCommand函数中增加了返回值和控制参数,用于指定后台服务组件的运行方式,其中最重要的返回值有三个。
❑Service. START_STICKY
如果Service.onStartCommand函数返回Service.START_STICKY,那么系统会对该服务组件“负责到底”,在强行回收该组件后,在资源宽裕的时候还会调用Service.onStartCommand函数去重新启动该服务。直到该服务组件执行完成所有操作后主动调用Service.stopSelf函数,才能最终关闭服务。
返回Service.START_STICKY的服务组件,是一个“不死”的组件。只要资源有空余,系统就会执行该服务,因此,此类服务往往与用户体验紧密结合。对于开发者而言,编写返回值为Service.START_STICKY的函数,一定要在合适的时机调用Service.stopSelf函数主动关闭服务,否则会无限期地消耗系统资源。
Android原生的音乐播放服务组件,在播放音乐时就是需要返回Service.START_STICKY来保持持续运行不被系统回收,使用户不会听着听着歌就没了:
//在消息循环中不断检查音乐播放状况,一旦发现音乐已经停止,就关闭服务
private Handler mDelayedStopHandler=new Handler(){
@Override
public void handleMessage(Message msg){
//检查音乐播放状况
if(!isPlaying()){
stopSelf();
}
}
};
public int onStartCommand(Intent intent, int flags, int startId){
…
//发送一个延迟消息,稍后检查音乐的播放状况
Message msg=mDelayedStopHandler.obtainMessage();
mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
//返回Service.START_STICKY,保持音乐播放器的运行状态
Return START_STICKY;
}
❑Service. START_NOT_STICKY
如果Service.onStartCommand函数返回Service.START_NOT_STICKY,则说明系统可以无条件地回收该组件,而无需关注服务是否完成,也不需要负责服务的重新启动。
通常,返回START_NOT_STICKY的服务,都是需要长时间运行服务组件,服务的时效性相对较低。为了在合适的时机重新启动服务,服务组件可以注册定时事件来保持长时间的运行。比如,一个后台邮件推送服务的示例如下:
/启动邮件推送服务PushService/
@Override
public int onStartCommand(Intent intent, int flags, int startId){
//开始监听新邮件
if(!wasStartListenMails()){
startListenMails();
}
//注册定时事件,每过3分钟自动唤醒服务,使得服务得以长期运行
final AlarmManager alarmManager=(AlarmManager)getSystemService(Context.
ALARM_SERVICE);
final PendingIntent weakupIntent=PendingIntent.getService(this,0,new
Intent(this, PushService.class),0);
alarmManager.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime()+180000,
weakupIntent);
//返回
return START_NOT_STICKY;
}
/停止邮件推送服务/
@Override
public void onDestroy(){
//停止监听,释放占用的线程等资源
stopListenMails();
super.onDestroy();
}
对于后台服务而言,如果与用户体验没有非常密切的联系,最好采取上述模式来维持其运行,而不要通过返回Service.START_STICKY来强制系统恢复,以免消耗过多的系统资源。
❑Service. START_REDELIVER_INTENT
如果Service.onStartCommand函数返回了Service.START_REDELIVER_INTENT,则意味着需要保证该服务组件能够完整地处理每个Intent对象。如果在服务的后台处理过程中,系统强行终止了该服务,则要求系统在资源允许的情况下重新传入该Intent对象,直到服务组件处理完成,显性地调用Service.selfStop来关闭服务为止。
返回Service.START_REDELIVER_INTENT的服务,关注的是Intent对象中的请求和数据,确保它们不会丢失。比如,如果调用组件传递给服务组件的Intent对象包含一些关键数据(比如用户输入的帐号和密码等),需要服务组件进行处理并存储,那么服务组件就要保证能够完整地执行整个操作,否则就会导致数据的丢失。
如果返回Service.START_REDELIVER_INTENT的服务组件,在后台执行的过程中被系统回收,则系统会在不久后重新调用Service.onStartCommand函数,再次传入原来的Intent对象,同时将flags参数标记为Service.START_FLAG_REDELIVERY,以区别首次调用与重新调用。
Android原生的日历应用的提醒服务(AlertService),就是一个返回Service.START_REDELIVER_INTENT的服务组件。当用户日历上有事件触发时,日历应用会向提醒服务发送Intent,提醒用户有事件需要处理。为了确保用户在任何时候下都能够收到该事件提醒,就需要返回Service.START_REDELIVER_INTENT使得系统不会丢弃任何一个发往该服务的Intent请求:
mServiceHandler=new Handler{
@Override
public void handleMessage(Message msg){
//处理提醒消息,根据消息内容来提示用户
processMessage(msg);
//如果没有更多的消息需要处理,主动关闭服务
TryToStopService();
}
};
public int onStartCommand(Intent intent, int flags, int startId){
if(intent!=null){
//从Intent对象中提取需要提醒的内容,并发送到消息循环中
Message msg=mServiceHandler.obtainMessage();
msg.arg1=startId;
msg.obj=intent.getExtras();//需要提醒的内容
mServiceHandler.sendMessage(msg);
}
//返回Service.START_REDELIVER_INTENT,保证可靠性
return START_REDELIVER_INTENT;
}
[1]在2.0以上的版本中,onStart函数已被onStartCommand函数所取代。