DroidPlugin是360開源的插件化框架,github地址爲:github.com/DroidPlugin…。 因公司業務及項目歷史緣由,來公司的這段時間一直在使用DroidPlugin進行業務開發,期間遇到的一些問題在此進行總結記錄。java
爲了方便訪客知道本章在解決什麼問題,這裏先把需求背景說明清楚。android
這裏的全部進程指的是產品app自己的【宿主進程】,與做爲插件安裝的遊戲【插件進程】。git
在咱們每次點擊Home按鍵時系統會發出action爲Intent.ACTION_CLOSE_SYSTEM_DIALOGS的廣播,用於關閉系統Dialog,此廣播能夠來監聽Home按鍵,這種方式是我目前用過的最好的。github
/** * @建立者 LQR * @時間 2019/1/7 * @描述 home鍵監聽 */
public class HomeEventWatcher extends BroadcastReceiver {
private Context mContext;
private HomeEventWatcher(Context context) {
mContext = context;
}
private static HomeEventWatcher INSTATNCE;
public static final HomeEventWatcher get(Context context) {
if (INSTATNCE == null) {
synchronized (HomeEventWatcher.class) {
if (INSTATNCE == null) {
INSTATNCE = new HomeEventWatcher(context.getApplicationContext());
}
}
}
return INSTATNCE;
}
/** * 註冊事件監聽(在onCreate()中執行) */
public HomeEventWatcher register() {
if (mHomeClickListener != null && mContext != null) {
IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
mContext.registerReceiver(this, filter);
}
return this;
}
/** * 反註冊事件監聽(在onDestroy()中執行) */
public void unRegister() {
mContext.unregisterReceiver(this);
}
/*------------------ 點擊事件監聽 begin ------------------*/
private static final class Home {
private static final String SYSTEM_DIALOG_REASON_KEY = "reason";
private static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
}
private OnHomeClickListener mHomeClickListener;
public HomeEventWatcher setHomeClickListener(OnHomeClickListener homeClickListener) {
mHomeClickListener = homeClickListener;
return this;
}
@Override
public void onReceive(Context context, Intent intent) {
String intentAction = intent.getAction();
// Log.i("MyAPP", "intentAction =" + intentAction);
// 按下home鍵事件
if (TextUtils.equals(intentAction, Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
String reason = intent.getStringExtra(Home.SYSTEM_DIALOG_REASON_KEY);
// Log.i("MyAPP", "reason =" + reason);
if (TextUtils.equals(Home.SYSTEM_DIALOG_REASON_HOME_KEY, reason)) {
if (mHomeClickListener != null) {
mHomeClickListener.onHomeClick();
}
}
}
// 其餘按鍵事件
// ...
}
/*------------------ 點擊事件監聽 end ------------------*/
public interface OnHomeClickListener {
void onHomeClick();
}
}
複製代碼
如下方法二選一:shell
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
複製代碼
注意,最好在確保app進程處於後臺進程時再執行,由於部分設備會自動重啓那些被強殺的前臺進程。或者,想辦法關閉全部的Activity,而後直接執行強殺,至於如何關閉全部Activity,下面會提供一種簡單粗暴的方法。架構
這裏提供2個adb指令,方便查看進程情況、強制結束進程。app
adb shell " procrank | grep com.xxx.yyy " // 查看進程情況(若進程不存在,則終端不顯示任何信息)
adb shell am force-stop com.xxx.yyy // 強制結束進程
複製代碼
注意: 1)com.xxx.yyy不是包名,而是applicationId,一般狀況下,包名與applicationId一致。 2)使用DroidPlugin運行的插件,會多出來一個插件進程,進程名通常爲 宿主進程名+PluginP07。框架
下面正式進入本章核心內容,情景前提:產品app在接收到home事件時,會執行進程自殺邏輯,殺死與當前app相關的全部進程。ide
啓動產品app,而後直接按home鍵,使用AndroidStudio觀察進程並查看日誌輸出,看到控制檯輸出了強殺日誌,而app進程在殺死後重啓了。gradle
經過日誌能夠肯定強殺代碼有被執行到,而且進程也被殺死過,這個進程重啓不是項目代碼觸發的,應該是DroidPlugin設置了相似保活機制的東西,致使Android系統拉起被強殺的產品app。經過查閱DroidPlugin源碼,能夠知道DroidPlugin會啓動一個Service,用來管理插件(安裝、卸載等),這個Service使用了start和bind方式啓動,而且設置前臺進程保活,代碼以下:
// =================== com.morgoo.droidplugin.pm.PluginManager ===================
public void connectToService() {
if (mPluginManager == null) {
try {
Intent intent = new Intent(mHostContext, PluginManagerService.class);
intent.setPackage(mHostContext.getPackageName());
mHostContext.startService(intent);
String auth = mHostContext.getPackageName() + ".plugin.servicemanager";
Uri uri = Uri.parse("content://" + auth);
Bundle args = new Bundle();
args.putString(PluginServiceProvider.URI_VALUE, "content://" + auth);
Bundle res = ContentProviderCompat.call(mHostContext, uri,
PluginServiceProvider.Method_GetManager,
null, args);
if (res != null) {
IBinder clientBinder = BundleCompat.getBinder(res, PluginServiceProvider.Arg_Binder);
onServiceConnected(intent.getComponent(), clientBinder);
} else {
mHostContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
}
} catch (Exception e) {
Log.e(TAG, "connectToService", e);
}
}
}
// =================== com.morgoo.droidplugin.PluginManagerService ===================
@Override
public void onCreate() {
super.onCreate();
keepAlive();
getPluginPackageManager(this);
}
private void keepAlive() {
try {
Notification notification = new Notification();
notification.flags |= Notification.FLAG_NO_CLEAR;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
startForeground(0, notification); // 設置爲前臺服務避免kill,Android4.3及以上須要設置id爲0時通知欄纔不顯示該通知;
} catch (Throwable e) {
e.printStackTrace();
}
}
複製代碼
應該大體能夠肯定,宿主進程殺不死的緣由,就是這個PluginManagerService致使的,處理方式有2種。
/** * 中止插件服務 */
private void stopPluginServer() {
Intent intent = new Intent();
intent.setClass(PluginManager.getInstance().getHostContext(), PluginManagerService.class);
CONTEXT.getApplicationContext().stopService(intent);
}
複製代碼
// =================== com.morgoo.droidplugin.pm.PluginManager ===================
public void connectToService() {
if (mPluginManager == null) {
try {
Intent intent = new Intent(mHostContext, PluginManagerService.class);
intent.setPackage(mHostContext.getPackageName());
// mHostContext.startService(intent);
...
mHostContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
} catch (Exception e) {
Log.e(TAG, "connectToService", e);
}
}
}
// =================== com.morgoo.droidplugin.PluginManagerService ===================
@Override
public void onCreate() {
super.onCreate();
// keepAlive();
getPluginPackageManager(this);
}
複製代碼
遊戲運行中,按下home鍵強殺app,點擊App icon再次啓動App,直接進入剛剛的遊戲。
在插件遊戲運行過程當中,打開終端或cmd,使用adb查看當前棧信息:
adb shell dumpsys activity activities top
複製代碼
能夠看到,遊戲進程(插件進程)與產品app進程(宿主進程)共用一個Activity棧,由此能夠推測,由於宿主App在被強殺的時候,系統保存了宿主進程的Activity棧信息,因此,在產品app下次啓動時,系統會恢復棧記錄。
根據前面的推測,針對目前的問題,方案無非就2個,要麼讓宿主進程在被強殺時不要被系統保存棧記錄,要麼讓宿主進程與插件進程不要共用一個棧。要注意,方案一纔是關鍵,但這個與第3個坑有關聯,因此,這裏就只說下方案二吧。很簡單,修改產品app(宿主)入口Activity的啓動模式便可,如把 launchMode 修改成 singleInstance,這樣的話,下次經過icon啓動產品app時,系統會單獨使用一個棧來存放這個入口Activity,從而避免與插件共用一個棧的問題。修改完成後,啓動產品app,再啓動遊戲插件,這時,經過adb命令查看當前棧信息:
adb shell dumpsys activity activities top
複製代碼
能夠看到產品app與遊戲插件不在一個棧內,這時,按home鍵,再啓動就不會再進入遊戲界面了。可是,方案二並非正確的解決辦法,方案一纔是,由於進程強殺前的棧信息仍是會被保留下來的,若是項目採用的是Activity + Fragment架構,這時,效果會很"神奇",這絕對不是產品但願看到的。那要怎樣才能讓進程在被強殺時不要被系統保存棧記錄呢?請繼續往下看。
進入產品app,啓動遊戲A,按home鍵,再進入產品app,啓動遊戲B,這時,直接啓動了遊戲A。
這就是前面問題2說到的,狀態保存問題,插件進程在按下home時被強殺,這時,系統認爲該遊戲插件是意外退出,會保存當前遊戲的狀態,以便下次啓動時恢復。要知道,DroidPlugin使用組件預先佔坑的方式,預先在宿主清單文件中聲明好多個Activity、Service等,而且會對組件進行復用,因此,當下次啓動另外一個遊戲時,恰好複用了前一個遊戲使用過的組件(Activity),因而在恢復狀態的時候,就把前一個遊戲恢復回來了。
以上分析我的猜想,不知說法是否正確,若有問題請不吝賜教~
遊戲(插件)退出時,銷燬遊戲全部的Activity,銷燬當前進程全部Activity的方法以下:
/** * 關閉當前App全部Activity */
public void finishAllActivities(Application application) {
List<Activity> activities = getActivitiesByApplication(application);
if (activities != null && activities.size() > 0) {
for (int i = activities.size() - 1; i >= 0; i--) {
Activity activity = activities.get(i);
activity.finish();
Log.e("lqr", "finish activity : " + activity);
}
}
}
/** * 獲取當前App中全部Activity */
public List<Activity> getActivitiesByApplication(Application application) {
List<Activity> list = new ArrayList<>();
try {
Class<Application> applicationClass = Application.class;
Field mLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
mLoadedApkField.setAccessible(true);
Object mLoadedApk = mLoadedApkField.get(application);
Class<?> mLoadedApkClass = mLoadedApk.getClass();
Field mActivityThreadField = mLoadedApkClass.getDeclaredField("mActivityThread");
mActivityThreadField.setAccessible(true);
Object mActivityThread = mActivityThreadField.get(mLoadedApk);
Class<?> mActivityThreadClass = mActivityThread.getClass();
Field mActivitiesField = mActivityThreadClass.getDeclaredField("mActivities");
mActivitiesField.setAccessible(true);
Object mActivities = mActivitiesField.get(mActivityThread);
// 注意這裏必定寫成Map,低版本這裏用的是HashMap,高版本用的是ArrayMap
if (mActivities instanceof Map) {
@SuppressWarnings("unchecked")
Map<Object, Object> arrayMap = (Map<Object, Object>) mActivities;
for (Map.Entry<Object, Object> entry : arrayMap.entrySet()) {
Object value = entry.getValue();
Class<?> activityClientRecordClass = value.getClass();
Field activityField = activityClientRecordClass.getDeclaredField("activity");
activityField.setAccessible(true);
Object o = activityField.get(value);
list.add((Activity) o);
}
}
} catch (Exception e) {
e.printStackTrace();
list = null;
}
return list;
}
複製代碼
注意:這個關閉全部Activity的方法能夠用來解決問題2最後遺留的問題。
要注意,DroidPlugin會爲每一個插件單首創建進程,也就是說,若是你項目中使用了DroidPlugin,就會涉及到多進程,在啓動插件時,宿主的Application內的邏輯會執行屢次(宿主、插件進程一建立就會執行),因此,建議在項目的自定義Application中對進程進行區分,根據不一樣進程分別處理(如:第三方面SDK只須要在產品app宿主進程中初始化),判斷當前進程是否爲插件進程的方法以下:
/** * 判斷當前進程是否爲插件進程 * * @param context 上下文 * @param hostAppId 宿主appid * @return */
public boolean adjustPluginProcess(Context context, String hostAppId) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = am.getRunningAppProcesses();
if (runningAppProcesses != null && runningAppProcesses.size() > 0) {
for (ActivityManager.RunningAppProcessInfo info : runningAppProcesses) {
// Step 1. 找到當前進程
if (info.pid == Process.myPid()) {
// Log.e("lqr", "info.processName = " + info.processName);
// Step 2. 判斷當前進程是否爲插件進程(依據)
return !info.processName.equals(hostAppId);
}
}
}
return false;
}
複製代碼
Q:爲何要傳入宿主的appid? A:這裏說的appid指的就是applicationId。由於appid不等同於包名,咱們常說的一個設備上不能安裝相同包名的app這種說法是不嚴謹的,應該是不能安裝相同appid的app,此外,一個項目在多渠道的狀況下,是能夠經過gradle來指定修改appid的,若是你的項目中有使用過多渠道打包,相信應該可以明白,綜上,包名不能做爲判斷宿主進程的依據,因此只能使用appid來判斷。 Q:爲何不以進程名是否帶有 "PluginP" 字樣來判斷是否爲插件進程? A:親測這種方式不許確,在有些設備上,插件進程的進程名是這樣的規則,但有些設備不是,直接是插件本來的applicationId。
經過上面的代碼,根據項目的具體狀況,分別處理宿主進程與插件進程吧,建議2個進程在監聽到home事件時,都關閉全部Activity,這樣系統就不會保存棧狀態了(必定要先關閉插件的,再關閉宿主的!!)。
公司是作盒子應用開發的,在部分4.x的盒子上確實出現了使用DroidPlugin沒法正常安裝插件的狀況,但舊版的DroidPlugin就不會,我比對了2個版本的DroidPlugin,最終定位到在com.morgoo.droidplugin.pm包下的PluginManager,其中有這麼一個方法:
新版的DroidPlugin適配了高版本的Android系統(如:Android8.0)
// =================== 舊版DroidPlugin ===================
public void connectToService() {
if (mPluginManager == null) {
try {
Intent intent = new Intent(mHostContext, PluginManagerService.class);
intent.setPackage(mHostContext.getPackageName());
mHostContext.startService(intent);
mHostContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
} catch (Exception e) {
Log.e(TAG, "connectToService", e);
}
}
}
// =================== 新版DroidPlugin ===================
public void connectToService() {
if (mPluginManager == null) {
try {
Intent intent = new Intent(mHostContext, PluginManagerService.class);
intent.setPackage(mHostContext.getPackageName());
mHostContext.startService(intent);
String auth = mHostContext.getPackageName() + ".plugin.servicemanager";
Uri uri = Uri.parse("content://" + auth);
Bundle args = new Bundle();
args.putString(PluginServiceProvider.URI_VALUE, "content://" + auth);
Bundle res = ContentProviderCompat.call(mHostContext, uri,
PluginServiceProvider.Method_GetManager,
null, args);
if (res != null) {
IBinder clientBinder = BundleCompat.getBinder(res, PluginServiceProvider.Arg_Binder);
onServiceConnected(intent.getComponent(), clientBinder);
} else {
mHostContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
}
} catch (Exception e) {
Log.e(TAG, "connectToService", e);
}
}
}
複製代碼
正是由於這部分多出來的代碼,致使新版的DroidPlugin沒法在個別4.x設備上正常安裝插件,因此,咱們能夠對源碼進行修改,區分4.x如下及高版本的代碼邏輯便可,如:
public void connectToService() {
if (mPluginManager == null) {
try {
Intent intent = new Intent(mHostContext, PluginManagerService.class);
intent.setPackage(mHostContext.getPackageName());
// mHostContext.startService(intent);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
mHostContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
} else {
String auth = mHostContext.getPackageName() + ".plugin.servicemanager";
Uri uri = Uri.parse("content://" + auth);
Bundle args = new Bundle();
args.putString(PluginServiceProvider.URI_VALUE, "content://" + auth);
Bundle res = ContentProviderCompat.call(mHostContext, uri,
PluginServiceProvider.Method_GetManager,
null, args);
if (res != null) {
IBinder clientBinder = BundleCompat.getBinder(res, PluginServiceProvider.Arg_Binder);
onServiceConnected(intent.getComponent(), clientBinder);
} else {
mHostContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
}
}
} catch (Exception e) {
Log.e(TAG, "connectToService", e);
}
}
}
複製代碼
以上,就是本人在實際開發中,使用DroidPlugin的項目在強殺時的踩坑記錄分享,若是有什麼更好的解決方案,但願能夠一塊兒交流,如文章中說明有問題歡迎指出交流,不喜勿噴。