由于我们做的是机器人上的软件,而机器人是24小时不间断服务的,这就要求我们的软件不能退出到系统桌面。当然最好是能够做到程序能够不卡顿,不崩溃,自己不退出。由于我们引用了很多第三方的开发包,也不能保证他们的稳定性,所以,要做到完全不崩溃也是不可能的。
退而求其次,如果崩溃了我们就要保证程序能够被拉起来,期间也看过很多保活的方案,比如service前台的方法,比如jni里写守护进程,比如接收系统广播唤醒,比如用alarmmanager唤醒等等,感觉不是效率底,就是被系统屏蔽了。经过不断筛选,我认为使用aidl进行双进程守护其实是效率很好的一个解决方案。
其实这个原理也很简单,简单的说就是创建两个service,其中一个再程序主进程,另外一个在其他进程,这两个进程通过aidl通信,一旦其中一个进程断开连接,那么就重启该服务,两个程序互相监听,就能够做到一方被杀死,另一方被启动了。当然,如果使用 adb shell force-stop packageName的方法杀死程序,肯定是不能够重启的。这种方式仅仅是为了避免ndk层崩溃,java抓不到从而不能使用java层重启应用的一种补充方式。要想做到完全不被杀死,那就太流氓了。
说了这么多,看代码吧
两个service,localservice和remoteservice
LocalService.java
1 package guide.yunji.com.guide.processGuard;
2
3 import android.app.Application;
4 import android.app.Service;
5 import android.content.ComponentName;
6 import android.content.Context;
7 import android.content.Intent;
8 import android.content.ServiceConnection;
9 import android.os.IBinder;
10 import android.os.RemoteException;
11 import android.util.Log;
12 import android.widget.Toast;
13
14 import guide.yunji.com.guide.MyApplication;
15 import guide.yunji.com.guide.activity.MainActivity;
16 import guide.yunji.com.guide.testFace.IMyAidlInterface;
17
18 public class LocalService extends Service {
19 private static final String TAG = LocalService.class.getName();
20 private MyBinder mBinder;
21
22 private ServiceConnection connection = new ServiceConnection() {
23 @Override
24 public void onServiceConnected(ComponentName name, IBinder service) {
25 IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
26 try {
27 Log.e("LocalService", "connected with " + iMyAidlInterface.getServiceName());
28 //TODO whh 本地service被拉起,检测如果mainActivity不存在则拉起
29 if (MyApplication.getMainActivity() == null) {
30 Intent intent = new Intent(LocalService.this.getBaseContext(), MainActivity.class);
31 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
32 getApplication().startActivity(intent);
33 }
34 } catch (RemoteException e) {
35 e.printStackTrace();
36 }
37 }
38
39 @Override
40 public void onServiceDisconnected(ComponentName name) {
41 Toast.makeText(LocalService.this, "链接断开,重新启动 RemoteService", Toast.LENGTH_LONG).show();
42 Log.e(TAG, "onServiceDisconnected: 链接断开,重新启动 RemoteService");
43 startService(new Intent(LocalService.this, RemoteService.class));
44 bindService(new Intent(LocalService.this, RemoteService.class), connection, Context.BIND_IMPORTANT);
45 }
46 };
47
48 public LocalService() {
49 }
50
51 @Override
52 public void onCreate() {
53 super.onCreate();
54 }
55
56 @Override
57 public int onStartCommand(Intent intent, int flags, int startId) {
58 Log.e(TAG, "onStartCommand: LocalService 启动");
59 Toast.makeText(this, "LocalService 启动", Toast.LENGTH_LONG).show();
60 startService(new Intent(LocalService.this, RemoteService.class));
61 bindService(new Intent(LocalService.this, RemoteService.class), connection, Context.BIND_IMPORTANT);
62 return START_STICKY;
63 }
64
65 @Override
66 public IBinder onBind(Intent intent) {
67 mBinder = new MyBinder();
68 return mBinder;
69 }
70
71 private class MyBinder extends IMyAidlInterface.Stub {
72
73 @Override
74 public String getServiceName() throws RemoteException {
75 return LocalService.class.getName();
76 }
77
78 @Override
79 public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
80
81 }
82 }
83 }
RemoteService.java
package guide.yunji.com.guide.processGuard;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;
import guide.yunji.com.guide.testFace.IMyAidlInterface;
public class RemoteService extends Service {
private static final String TAG = RemoteService.class.getName();
private MyBinder mBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IMyAidlInterface iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
try {
Log.e(TAG, "connected with " + iMyAidlInterface.getServiceName());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.e(TAG, "onServiceDisconnected: 链接断开,重新启动 LocalService");
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) {
Log.e(TAG, "onStartCommand: RemoteService 启动");
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();
}
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
}
}
注意,两个service要在不通的进程
1 <service
2 android:name=".processGuard.LocalService"
3 android:enabled="true"
4 android:exported="true" />
5 <service
6 android:name=".processGuard.RemoteService"
7 android:enabled="true"
8 android:exported="true"
9 android:process=":RemoteProcess" />
两个service通过aidl连接,如下
1 // IMyAidlInterface.aidl
2 package guide.yunji.com.guide.testFace;
3
4 // Declare any non-default types here with import statements
5
6 interface IMyAidlInterface {
7 /**
8 * Demonstrates some basic types that you can use as parameters
9 * and return values in AIDL.
10 */
11 void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
12 double aDouble, String aString);
13 String getServiceName();
14 }
此外还要注意一点,程序的service初始化的时候如果在自定义的application的时候要注意多进程的问题,本来LocalService是在主进程中启动的,所以要做一下进程的判断,如下:
1 package com.honghe.guardtest;
2
3 import android.app.ActivityManager;
4 import android.app.Application;
5 import android.content.Context;
6 import android.content.Intent;
7
8 public class MyApplication extends Application {
9 private static MainActivity mainActivity = null;
10
11 public static MainActivity getMainActivity() {
12 return mainActivity;
13 }
14
15 public static void setMainActivity(MainActivity activity) {
16 mainActivity = activity;
17 }
18
19 @Override
20 public void onCreate() {
21 super.onCreate();
22 if (isMainProcess(getApplicationContext())) {
23 startService(new Intent(this, LocalService.class));
24 } else {
25 return;
26 }
27 }
28
29 /**
30 * 获取当前进程名
31 */
32 public String getCurrentProcessName(Context context) {
33 int pid = android.os.Process.myPid();
34 String processName = "";
35 ActivityManager manager = (ActivityManager) context.getApplicationContext().getSystemService
36 (Context.ACTIVITY_SERVICE);
37 for (ActivityManager.RunningAppProcessInfo process : manager.getRunningAppProcesses()) {
38 if (process.pid == pid) {
39 processName = process.processName;
40 }
41 }
42 return processName;
43 }
44
45 public boolean isMainProcess(Context context) {
46 /**
47 * 是否为主进程
48 */
49 boolean isMainProcess;
50 isMainProcess = context.getApplicationContext().getPackageName().equals
51 (getCurrentProcessName(context));
52 return isMainProcess;
53 }
54 }
然后LocalService重启后,可以判断是否要开启程序的主界面,上面的localService已经写了,就不多介绍了。
代码已经有了,我们怎么测试呢?
当然是伪造一个ndk的崩溃来验证程序的可行性了。
我们写一个jni,如下
写一个Jni的类
JniLoaderndk.cpp
1 #include <string.h>
2 #include <jni.h>
3 #include <stdio.h>
4
5 //#include "yue_excample_hello_JniLoader.h"
6 //按照C语言规则编译。jni依照C的规则查找函数,而不是C++,没有这一句运行时会崩溃报错:
7 // java.lang.UnsatisfiedLinkError: Native method not found:
8 extern "C"{
9
10 JNIEXPORT jstring JNICALL Java_com_honghe_guardtest_JniLoader_getHelloString
11 (JNIEnv *env, jobject _this)
12 {
13 int m=30;
14 int n=0;
15 int j=m/n;
16 printf("hello %d",j);
17 Java_com_honghe_guardtest_JniLoader_getHelloString(env,_this);
18 //return (*env)->NewStringUTF(env, "Hello world from jni)");//C语言格式,文件名应为xxx.c
19 return env->NewStringUTF((char *)("hello whh"));//C++格式,文件名应为xxx.cpp
20 }
21
22
23 }
为什么这么写,因为我本来想通过除0来制造异常,但是ndk本身并不向上层因为除0崩溃,后来无奈只好使用递归来制造崩溃了。
android.mk
1 LOCAL_PATH := $(call my-dir)
2 include $(CLEAR_VARS)
3
4 # 要生成的.so库名称。java代码System.loadLibrary("firstndk");加载的就是它
5 LOCAL_MODULE := firstndk
6
7 # C++文件
8 LOCAL_SRC_FILES := JniLoaderndk.cpp
9
10 include $(BUILD_SHARED_LIBRARY)
application.mk
# 注释掉了,不写会生成全部支持的平台。目前支持:
APP_ABI := armeabi arm64-v8a armeabi-v7a mips mips64 x86 x86_64
#APP_ABI := armeabi-v7a
写完了ndk后需要到jni的目录下执行一个 ndk-build 的命令,这样会在main目录下生成libs文件夹,文件夹中有目标平台的so文件,但是android默认不会读取该目录的so文件,所以我们需要在app/build.gradle中加入路径,使程序能够识别so
sourceSets {
main {
jniLibs.srcDirs = ['src/main/libs']//默认为jniLibs
}
}
弄好后,就可以在安卓程序中找到ndk中的方法了。
创建调用类
JniLoader.java
1 package com.honghe.guardtest;
2
3 public class JniLoader {
4 static {
5 System.loadLibrary("firstndk");
6 }
7
8 public native String getHelloString();
9 }
调用该方法就能够发现程序在ndk影响下崩溃了,如图
看logcat
说明旧的进程由于ndk崩溃被杀死了,但是看界面里程序已经重启了,然后还多出了一个不通pid的同名进程,如下
证明ndk崩溃后我们的软件重启成功了。