背景
最近单位项目里要用到双服务进程保活,目的是要保证服务不被杀死。
双进程保活实际是两个进程相互监听,在各自的销毁回调方法里,启动对方。网上有一个不错的双服务进程保活开源框架,叫做HelloDaemon,github地址:
https://github.com/xingda920813/HelloDaemon
现在,记录一下对于其的源码阅读过程
源码阅读
AbsWorkService
我们的工作(服务)进程要继承于AbsWorkService,启动时要调用onBind()和onStartCommand()方法
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return onStart(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
onStart(intent, 0, 0);
return onBind(intent, null);
}
其中onBind()里的onBind(intent, null)是留给子类实现的,用于bind时的业务逻辑。因此关键就是onStart()方法
onStart()
/**
* 1.防止重复启动,可以任意调用 DaemonEnv.startServiceMayBind(Class serviceClass);
* 2.利用漏洞启动前台服务而不显示通知;
* 3.在子线程中运行定时任务,处理了运行前检查和销毁时保存的问题;
* 4.启动守护服务;
* 5.守护 Service 组件的启用状态, 使其不被 MAT 等工具禁用.
*/
protected int onStart(Intent intent, int flags, int startId) {
//启动守护服务,运行在:watch子进程中
DaemonEnv.startServiceMayBind(WatchDogService.class);
//业务逻辑: 实际使用时,根据需求,将这里更改为自定义的条件,判定服务应当启动还是停止 (任务是否需要运行)
Boolean shouldStopService = shouldStopService(intent, flags, startId);
if (shouldStopService != null) {
if (shouldStopService) stopService(intent, flags, startId); else startService(intent, flags, startId);
}
if (mFirstStarted) {
mFirstStarted = false;
//启动前台服务而不显示通知的漏洞已在 API Level 25 修复,大快人心!
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
//利用漏洞在 API Level 17 及以下的 Android 系统中,启动前台服务而不显示通知
startForeground(HASH_CODE, new Notification());
//利用漏洞在 API Level 18 及以上的 Android 系统中,启动前台服务而不显示通知
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
DaemonEnv.startServiceSafely(new Intent(getApplication(), WorkNotificationService.class));
}
getPackageManager().setComponentEnabledSetting(new ComponentName(getPackageName(), WatchDogService.class.getName()),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}
return START_STICKY;
}
可分四步:
1、启动看门狗
2、停止或启动自身
3、初次调用时,启动前台服务并不显示通知
4、设置看门狗所在应用不被杀死
首先启动了一个WatchDogService,也就是看门狗服务,它就是用来每隔一段时间启动工作进程的。至于为何说DaemonEnv.startServiceMayBind()方法可以任意调用而不怕服务重复启动(实则时不怕服务重复绑定),可以点进去看看源码的实现
DaemonEnv#startServiceMayBind()
public static void startServiceMayBind(@NonNull final Class<? extends Service> serviceClass) {
if (!sInitialized) return;
final Intent i = new Intent(sApp, serviceClass);
startServiceSafely(i);
ServiceConnection bound = BIND_STATE_MAP.get(serviceClass);
if (bound == null) sApp.bindService(i, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
BIND_STATE_MAP.put(serviceClass, this);
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
BIND_STATE_MAP.remove(serviceClass);
startServiceSafely(i);
if (!sInitialized) return;
sApp.bindService(i, this, Context.BIND_AUTO_CREATE);
}
}, Context.BIND_AUTO_CREATE);
}
sInitialized说明在使用前(启动工作服务前要初始化),而后实例化一个意图,调用startServiceSafely()
static void startServiceSafely(Intent i) {
if (!sInitialized) return;
try { sApp.startService(i); } catch (Exception ignored) {}
}
其实就是启动了服务。
返回DaemonEnv.startServiceMayBind(),看它对服务绑定是怎么处理的,显然DaemonEnv内部用了一个映射来保存service和对应的connection,在绑定前,先看service对应的connection是不是空,是空说明是头一次绑定,否则就不是;对于头一次绑定的服务,在绑定时new出来的connection的回调里,连接时的回调就是把service和connection的对应关系保存,断开时的回调则是重复startServiceMayBind()过程,保证已绑定的服务不会解绑。(尽管onServiceDisconnected()里代码是写了四行,但通过和DaemonEnv.startServiceMayBind()方法比较,显然是又重复了一遍过程,只不过在给映射put前,先删除了原数据)
服务的停止和启动
回到工作进程的onStart()方法,DaemonEnv.startServiceMayBind()启动看门狗后,根据是否停止服务来进行服务的停止和启动
Boolean shouldStopService = shouldStopService(intent, flags, startId);
if (shouldStopService != null) {
if (shouldStopService) stopService(intent, flags, startId); else startService(intent, flags, startId);
}
shouldStopService()要子类实现,而后我们看看stopService()和startService()方法。stopService()方法源码如下:
void stopService(Intent intent, int flags, int startId) {
//取消对任务的订阅
stopWork(intent, flags, startId);
//取消 Job / Alarm / Subscription
cancelJobAlarmSub();
}
stopWork()留给子类实现,cancelJobAlarmSub()方法以发送广播的形式结束对jobScheduler、AlaramManager和disposable的订阅,也就取消了看门狗对服务进程的定时启动(开来并不是真正结束了服务,而是取消了守护)
public static void cancelJobAlarmSub() {
if (!DaemonEnv.sInitialized) return;
DaemonEnv.sApp.sendBroadcast(new Intent(WakeUpReceiver.ACTION_CANCEL_JOB_ALARM_SUB));
}
public class WakeUpReceiver extends BroadcastReceiver {
/**
* 向 WakeUpReceiver 发送带有此 Action 的广播, 即可在不需要服务运行的时候取消 Job / Alarm / Subscription.
*
* 监听 8 种系统广播 :
* CONNECTIVITY\_CHANGE, USER\_PRESENT, ACTION\_POWER\_CONNECTED, ACTION\_POWER\_DISCONNECTED,
* BOOT\_COMPLETED, MEDIA\_MOUNTED, PACKAGE\_ADDED, PACKAGE\_REMOVED.
* 在网络连接改变, 用户屏幕解锁, 电源连接 / 断开, 系统启动完成, 挂载 SD 卡, 安装 / 卸载软件包时拉起 Service.
* Service 内部做了判断,若 Service 已在运行,不会重复启动.
* 运行在:watch子进程中.
*/
protected static final String ACTION_CANCEL_JOB_ALARM_SUB = "com.xdandroid.hellodaemon.CANCEL_JOB_ALARM_SUB";
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && ACTION_CANCEL_JOB_ALARM_SUB.equals(intent.getAction())) {
WatchDogService.cancelJobAlarmSub();
return;
}
if (!DaemonEnv.sInitialized) return;
DaemonEnv.startServiceMayBind(DaemonEnv.sServiceClass);
}
.. // WakeUpAutoStartReceiver,暂时没有用到
}
发送指定action的广播后,如果是要取消服务,receiver就调用看门狗里的cancelJobAlarmSub()方法,结束对jobScheduler、AlaramManager和disposable的订阅。
public static void cancelJobAlarmSub() {
if (!DaemonEnv.sInitialized) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
JobScheduler scheduler = (JobScheduler) DaemonEnv.sApp.getSystemService(JOB_SCHEDULER_SERVICE);
scheduler.cancel(HASH_CODE);
} else {
AlarmManager am = (AlarmManager) DaemonEnv.sApp.getSystemService(ALARM_SERVICE);
if (sPendingIntent != null) am.cancel(sPendingIntent);
}
if (sDisposable != null) sDisposable.dispose();
}
回到工作进程的onStart(),下一步就是启动前台服务并不显示通知了
启动前台服务并不显示通知
如果是首次启动工作服务,就把这个服务设置为前台服务,但不显示通知。具体做法根据sdk版本不同而不同:对于25及以上,就不用管;18-24,要连续调用两次startForeground();17及以下,调用一次就可以。不过在调用startForeground()时,要保证id不等于0,否则就不是前台服务了
对于sdk是18-24,它启动了一个WorkNotificationService,其实这个service很简单
public static class WorkNotificationService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(AbsWorkService.HASH_CODE, new Notification());
stopSelf();
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
第二次调用startForeground()后就把自己停掉了。
注意HelloDaemon里所有服务的onStartCommand()方法都是返回START_STICK,表示此服务可以在任何时候被调起或被停止,所传入的intent可以为null
回到工作进程的onStart(),在头一次启动最后,他设置了不要杀死看门狗服务所在的应用
看门狗服务
方才说到,工作进程启动时会启动看门狗服务,我们就看一下看门狗WatchDogService的onBind()和onStartCommand()方法
@Override
public final int onStartCommand(Intent intent, int flags, int startId) {
return onStart(intent, flags, startId);
}
@Override
public final IBinder onBind(Intent intent) {
onStart(intent, 0, 0);
return null;
}
和工作进程一样,都是调用了自己的onStart()方法
onStart()
protected final int onStart(Intent intent, int flags, int startId) {
if (!DaemonEnv.sInitialized) return START_STICKY;
if (sDisposable != null && !sDisposable.isDisposed()) return START_STICKY;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
startForeground(HASH_CODE, new Notification());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
DaemonEnv.startServiceSafely(new Intent(DaemonEnv.sApp, WatchDogNotificationService.class));
}
//定时检查 AbsWorkService 是否在运行,如果不在运行就把它拉起来
//Android 5.0+ 使用 JobScheduler,效果比 AlarmManager 好
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
JobInfo.Builder builder = new JobInfo.Builder(HASH_CODE, new ComponentName(DaemonEnv.sApp, JobSchedulerService.class));
builder.setPeriodic(DaemonEnv.getWakeUpInterval());
//Android 7.0+ 增加了一项针对 JobScheduler 的新限制,最小间隔只能是下面设定的数字
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) builder.setPeriodic(JobInfo.getMinPeriodMillis(), JobInfo.getMinFlexMillis());
builder.setPersisted(true);
JobScheduler scheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
scheduler.schedule(builder.build());
} else {
//Android 4.4- 使用 AlarmManager
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
Intent i = new Intent(DaemonEnv.sApp, DaemonEnv.sServiceClass);
sPendingIntent = PendingIntent.getService(DaemonEnv.sApp, HASH_CODE, i, PendingIntent.FLAG_UPDATE_CURRENT);
am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + DaemonEnv.getWakeUpInterval(), DaemonEnv.getWakeUpInterval(), sPendingIntent);
}
//使用定时 Observable,避免 Android 定制系统 JobScheduler / AlarmManager 唤醒间隔不稳定的情况
sDisposable = Flowable
.interval(DaemonEnv.getWakeUpInterval(), TimeUnit.MILLISECONDS)
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
DaemonEnv.startServiceMayBind(DaemonEnv.sServiceClass);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
throwable.printStackTrace();
}
});
//守护 Service 组件的启用状态, 使其不被 MAT 等工具禁用
getPackageManager().setComponentEnabledSetting(new ComponentName(getPackageName(), DaemonEnv.sServiceClass.getName()),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
return START_STICKY;
}
可分五步:
1、判断是否初始化,以及是否取消了对disposable的订阅,如果是的话,直接返回
2、开启前台进程,不显示通知
3、根据sdk的版本不同,启动jobScheduler或闹钟的定时,定时启动服务进程
4、用disposable再次实现定时启动服务进程,它可以表示看门狗的服务状态
5、设置工作进程所在的应用不被杀死
有了服务进程的阅读记录,看门狗的onStart()就很简单了,但我不明白既然有了disposable定时,为何还要用jobScheduler和闹钟的定时。
服务的手动结束或退出
当我们手动结束工作服务或看门狗时(设置里退出或打开进程管理器退出),会回调各自的onDestroy()或onTaskRemoved()方法,而这两个方法在两个类里的实现是一样的
/**
* 设置-正在运行中停止服务时回调
*/
@Override
public void onDestroy() {
onEnd(null);
}
/**
* 最近任务列表中划掉卡片时回调
*/
@Override
public void onTaskRemoved(Intent rootIntent) {
onEnd(rootIntent);
}
protected void onEnd(Intent rootIntent) {
if (!DaemonEnv.sInitialized) return;
DaemonEnv.startServiceMayBind(DaemonEnv.sServiceClass);
DaemonEnv.startServiceMayBind(WatchDogService.class);
}
其实就是把他们俩重新启动一回
结语
通过对HelloDaemon源码的阅读,我们可以发现:
1、工作进程和看门狗相互监督守护,工作进程启动看门狗,看门狗定时启动工作进程
2、DaemonEnv则保证每个服务的绑定只会绑定一次,而且绑定后就不会解绑
3、所谓终止服务,其实只是取消了工作进程的定时启动,并没有直接停止进程
4、如果要手动关闭进程的话,只能导致服务的重启。
可以说提供了一个双进程保活的思路。