前言
最近有在项目中用到高德的定位SDK,功能是每隔一定的时间获取一次用户的地理位置,采取的方案是在后台开启一个 Service,监听高德地图的位置变化。
该功能在用户手机屏幕亮时完美实现,但是当屏幕被关闭的时候,位置信息却无法被获取了,经过原因的排查,发现是由于在用户手机息屏后,后台的 Service 被系统清除,所以功能无法起作用,也就是所谓的进程被杀了。
杀进程,一方面是因为手机内存不足,另一方面其实是 Google 从用户的方面考虑,把一些常驻后台的程序通过一定的算法进行管理,将那些过度消耗系统资源的流氓软件杀除,保证手机的性能和续航。但是有的软件,像定位这类的必须要保持后台的运行,如何才能避免被系统杀掉呢。其实避免被杀进程很难做到,除非是像微信、QQ、支付宝这类系统厂商认可的软件被官方加入白名单可以避免被杀进程。那其他的小软件怎么办,我们可以另辟蹊径,无法避免被杀进程,那就让我们的软件在被杀进程后,能自动重启。
我这里介绍一下双进程守护的方法,来实现进程被杀后的拉起。
双进程守护
双进程守护的思想就是,两个进程共同运行,如果有其中一个进程被杀,那么另一个进程就会将被杀的进程重新拉起,相互保护,在一定的意义上,维持进程的不断运行。
双进程守护的两个进程,一个进程用于我们所需的后台操作,且叫它本地进程,另一个进程只负责监听着本地进程的状态,在本地进程被杀的时候拉起,于此同时本地进程也在监听着这个进程,准备在它被杀时拉起,我们将这个进程称为远端进程。
由于在 Android 中,两个进程之间无法直接交互,所以我们这里还要用到 AIDL (Android interface definition Language ),进行两个进程间的交互。
代码实现
先来看一下demo代码结构,结构很简单,我这里创建了一个 Activity 作为界面,以及两个 Service ,一个是后台操作的 本地Service,另一个是守护进程的 远端Service,还有一个 AIDL文件用作进程间交互用。
项目结构
Activity 的定义很简单,就几个按钮,控制 Service 的状态,我这边定义了三个按钮,一个是开启后台服务,另外两个分别是关闭本地Service和远端的Service。
/** * @author chaochaowu */ public class GuardActivity extends AppCompatActivity { @BindView(R.id.button) Button button; @BindView(R.id.button2) Button button2; @BindView(R.id.button3) Button button3; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSupportActionBar().hide(); setContentView(R.layout.activity_guard); ButterKnife.bind(this); } @OnClick({R.id.button, R.id.button2, R.id.button3}) public void onViewClicked(View view) { switch (view.getId()) { case R.id.button: startService(new Intent(this, LocalService.class)); break; case R.id.button2: stopService(new Intent(this, LocalService.class)); break; case R.id.button3: stopService(new Intent(this, RemoteService.class)); break; default: break; } } }
可以看一下界面。
主界面
AIDL文件可以根据业务需要添加接口。
/** * @author chaochaowu */ interface IMyAidlInterface { String getServiceName(); }
重点是在两个 Service 上。
在定义Service时,需要在 AndroidManifest 中声明一下 远端Service 的 process 属性,保证 本地Service 和 远端Service 两者跑在不同的进程上,如果跑在同一个进程上,该进程被杀,那就什么都没了,就没有了双进程守护的说法了。
先来看 LocalService 的代码,重点关注 onStartCommand 方法 和 ServiceConnection 中重写的方法。onStartCommand 方法是在 Service 启动后被调用,在 LocalService 被启动后,我们将 RemoteService 进行了启动,并将 LocalService 和 RemoteService 两者绑定了起来(因为远端Service 对于用户来说是不可见的,相对于我们实际工作的进程也是独立的,它的作用仅仅是守护线程,所以说 RemoteService 仅与 LocalService 有关系,应该只能由 LocalService 将它启动)。
启动并绑定之后,我们需要重写 ServiceConnection 中的方法,监听两者之间的绑定关系,关键的是对两者绑定关系断开时的监听。
当其中一个进程被杀掉时,两者的绑定关系就会被断开,触发方法 onServiceDisconnected ,所以,我们要在断开时,进行进程拉起的操作,重写 onServiceDisconnected 方法,在方法中将另外一个 Service 重新启动,并将两者重新绑定。
/** * @author chaochaowu */ public class LocalService extends Service { private MyBinder mBinder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service); try { Log.i("LocalService", "connected with " + iMyAidlInterface.getServiceName()); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { Toast.makeText(LocalService.this,"链接断开,重新启动 RemoteService",Toast.LENGTH_LONG).show(); startService(new Intent(LocalService.this,RemoteService.class)); bindService(new Intent(LocalService.this,RemoteService.class),connection, Context.BIND_IMPORTANT); } }; public LocalService() { } @Override public void onCreate() { super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Toast.makeText(this,"LocalService 启动",Toast.LENGTH_LONG).show(); startService(new Intent(LocalService.this,RemoteService.class)); bindService(new Intent(this,RemoteService.class),connection, Context.BIND_IMPORTANT); return START_STICKY; } @Override public IBinder onBind(Intent intent) { mBinder = new MyBinder(); return mBinder; } private class MyBinder extends IMyAidlInterface.Stub{ @Override public String getServiceName() throws RemoteException { return LocalService.class.getName(); } } }
在另外一个 RemoteService 中也一样,在与 LocalService 断开链接的时候,由于监听到绑定的断开,说明 RemoteService 还存活着,LocalService 被杀进程,所以要将 LocalService 进行拉起,并重新绑定。方法写在 onServiceDisconnected 中。
/** * @author chaochaowu */ public class RemoteService extends Service { private MyBinder mBinder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service); try { Log.i("RemoteService", "connected with " + iMyAidlInterface.getServiceName()); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { Toast.makeText(RemoteService.this,"链接断开,重新启动 LocalService",Toast.LENGTH_LONG).show(); startService(new Intent(RemoteService.this,LocalService.class)); bindService(new Intent(RemoteService.this,LocalService.class),connection, Context.BIND_IMPORTANT); } }; public RemoteService() { } @Override public int onStartCommand(Intent intent, int flags, int startId) { Toast.makeText(this,"RemoteService 启动",Toast.LENGTH_LONG).show(); bindService(new Intent(this,LocalService.class),connection,Context.BIND_IMPORTANT); return START_STICKY; } @Override public IBinder onBind(Intent intent) { mBinder = new MyBinder(); return mBinder; } private class MyBinder extends IMyAidlInterface.Stub{ @Override public String getServiceName() throws RemoteException { return RemoteService.class.getName(); } } }
运行效果
启动 Activity 点击开启 LocalService 启动本地服务,提示中可以看到, LocalService 启动后 RemotService 守护线程也被启动。此时,两者已经绑定在了一起。
开启服务
点击关闭 LocalService 模拟本地进程被杀,Toast 提示链接断开,并尝试重新启动 LocalService,第二个Toast 提示 LocalService 被重新拉起。
关闭本地服务
点击关闭 RemoteService 模拟远端进程被杀,Toast 提示链接断开,并尝试重新启动 RemoteService ,第二个Toast 提示 RemoteService 被重新拉起。
关闭远端服务
可以发现,无论我们怎么杀进程,进程都会被重新拉起,这就达到了 Service 保活,双进程相互守护的目的。
总结
在开发的过程中总是有些无法避免的麻烦,但是方法总比困难多,耐心研究研究就行了。关于进程的保活,其实是没有办法的办法,我们应该尽量避免将进程常驻后台,如果真的需要,在完成后台工作后,也要及时将他们销毁。否则后台进程无端地消耗系统资源,用户又不知道,咱们的软件就也就成了流氓软件。开发人员应该有自己的良心,嗯。