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的项目在强杀时的踩坑记录分享,若是有什么更好的解决方案,但愿能够一块儿交流,如文章中说明有问题欢迎指出交流,不喜勿喷。