由於我們做的是機器人上的軟件,而機器人是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崩潰后我們的軟件重啟成功了。
代碼全部在github,如下
https://github.com/dongweiq/guardTest
我的github地址:https://github.com/dongweiq/study
歡迎關注,歡迎star o(∩_∩)o 。有什么問題請郵箱聯系 dongweiqmail@gmail.com qq714094450