插件sdk环境:com.qihoo360.replugin:replugin-host-lib:2.3.1
360Replugin官网:https://github.com/Qihoo360/RePlugin
最近做的项目基于360的Replugin插件化框架,测试在测试安装外置插件时,测试失败(一脸懵逼)。通过log看到,install返回的pluginInfo一直为null,插件一直安装失败,重启app,也无法找到已安装的此插件。不得已只能通过查看源码,进行分析。
1、第一部通过 RePlugin.install(path)寻找逻辑流程:
RePlugin.install---->MP.pluginDownloaded------>PluginProcessMain.getPluginHost().pluginDownloaded 异常,返回pluginInfo == null
/**
* 安装或升级此插件
* 注意:
* 1、这里只将APK移动(或复制)到“插件路径”下,不释放优化后的Dex和Native库,不会加载插件
* 2、支持“纯APK”和“p-n”(旧版,即将废弃)插件
* 3、此方法是【同步】的,耗时较少
* 4、不会触发插件“启动”逻辑,因此只要插件“当前没有被使用”,再次调用此方法则新插件立即生效
*
* @param path 插件安装的地址。必须是“绝对路径”。通常可以用context.getFilesDir()来做
* @return 安装成功的插件信息,外界可直接读取
* @since 2.0.0 (1.x版本为installDelayed)
*/
public static PluginInfo install(String path) {
if (TextUtils.isEmpty(path)) {
throw new IllegalArgumentException();
}// 判断文件合法性
File file = new File(path);
if (!file.exists()) {
if (LogDebug.LOG) {
LogDebug.e(TAG, "install: File not exists. path=" + path);
}
return null;
} else if (!file.isFile()) {
if (LogDebug.LOG) {
LogDebug.e(TAG, "install: Not a valid file. path=" + path);
}
return null;
}// 若为p-n开头的插件,则必须是从宿主设置的“插件安装路径”上(默认为files目录)才能安装,其余均不允许
if (path.startsWith("p-n-")) {
String installPath = RePlugin.getConfig().getPnInstallDir().getAbsolutePath();
if (!path.startsWith(installPath)) {
if (LogDebug.LOG) {
LogDebug.e(TAG, "install: Must be installed from the specified path. Path=" + path + "; Allowed=" + installPath);
}
return null;
}
}return MP.pluginDownloaded(path);
}
/**
* @param path
* @return
*/
public static final PluginInfo pluginDownloaded(String path) {
if (LOG) {
LogDebug.d(PLUGIN_TAG, "MP.pluginDownloaded ... path=" + path);
}/**
* 问题描述:
*
* 对于正在生效的插件,如果当前时机pluginHost没有存活,那么这里会先启动pluginHost,然后再调用它的PluginHost进程的pluginDownloaded接口
*
* 这里的问题是:pluginHost进程在启动过程会通过扫描文件的方式将当前即将生效的插件识别到,
* 而在进程ready后,再去调用pluginDownloaded接口的时候会认为它不是新插件,从而不会通过NEW_PLUGIN广播来周知所有进程新插件生效了
* 因此,当前进程也不会同步新插件生效的逻辑。
* so,问题就来了,当前进程新下载的插件由于pluginHost的逻辑无法正常生效。
* 当然该问题只针对p-n格式的插件,而纯APK格式的插件不再使用进程启动的时候通过扫描文件目录的方式来来识别所有准备好的插件
*
* 解决办法:
* 对于处于该流程的插件文件(p-n插件)加上lock文件,以表示当前插件正在生效,不需要plugHost进程再次扫描生效了,也就不存在新插件在新进程中成为了老插件
*/ProcessLocker lock = null;
try {
if (path != null) {
File f = new File(path);
String fileName = f.getName();
String fileDir = f.getParent();
if (fileName.startsWith("p-n-")) {
lock = new ProcessLocker(RePluginInternal.getAppContext(), fileDir, fileName + ".lock");
}
}if (lock != null && !lock.tryLock()) {
// 加锁
if (LOG) {
LogDebug.d(PLUGIN_TAG, "MP.pluginDownloaded ... lock file + " + path + " failed! ");
}
}PluginInfo info = PluginProcessMain.getPluginHost().pluginDownloaded(path);
if (info != null) {
RePlugin.getConfig().getEventCallbacks().onInstallPluginSucceed(info);
}
return info;
} catch (Throwable e) {
if (LOGR) {
LogRelease.e(PLUGIN_TAG, "mp.pded: " + e.getMessage(), e);
}
} finally {
// 去锁
if (lock != null) {
lock.unlock();
}
}
return null;
}
/**
* sPluginHostLocal 常驻进程使用,非常驻进程为null buyuntao
* sPluginHostRemote 非常驻进程使用,常驻进程为null,用于非常驻进程连接常驻进程 buyuntao
* @hide 内部框架使用
*/
public static final IPluginHost getPluginHost() {
if (sPluginHostLocal != null) {
return sPluginHostLocal;
}
// 可能是第一次,或者常驻进程退出了
if (sPluginHostRemote == null) {
if (LogDebug.LOG) {
if (IPC.isPersistentProcess()) {
LogDebug.e(PLUGIN_TAG, "插件框架未正常初始化");
throw new RuntimeException("插件框架未正常初始化");
}
}
// 再次唤起常驻进程
connectToHostSvc();
}
return sPluginHostRemote;
}
2、继续查找null是如何产生的:
PluginProcessMain.getPluginHost().pluginDownloaded----->IPluginHost.pluginDownloaded,源码如下:
public PluginInfo pluginDownloaded(String path) throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();PluginInfo _result;
try {
_data.writeInterfaceToken("com.qihoo360.loader2.IPluginHost");
_data.writeString(path);
this.mRemote.transact(18, _data, _reply, 0);
_reply.readException();
if (0 != _reply.readInt()) {
_result = (PluginInfo)PluginInfo.CREATOR.createFromParcel(_reply);
} else {
_result = null;
}
} finally {
_reply.recycle();
_data.recycle();
}return _result;
}
通过debug看到,最终的返回的null,是这一步返回: _result = null;上一步transact方法,Server和Client使用IBinder进行数据交互出了问题,如下源码Binder.java:
public final boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply,
int flags) throws RemoteException {
if (false) Log.v("Binder", "Transact: " + code + " to " + this);if (data != null) {
data.setDataPosition(0);
}
boolean r = onTransact(code, data, reply, flags);
if (reply != null) {
reply.setDataPosition(0);
}
return r;
}
private boolean execTransact(int code, long dataObj, long replyObj,
int flags) {
BinderCallsStats binderCallsStats = BinderCallsStats.getInstance();
BinderCallsStats.CallSession callSession = binderCallsStats.callStarted(this, code);
Parcel data = Parcel.obtain(dataObj);
Parcel reply = Parcel.obtain(replyObj);
// theoretically, we should call transact, which will call onTransact,
// but all that does is rewind it, and we just got these from an IPC,
// so we'll just call it directly.
boolean res;
// Log any exceptions as warnings, don't silently suppress them.
// If the call was FLAG_ONEWAY then these exceptions disappear into the ether.
final boolean tracingEnabled = Binder.isTracingEnabled();
try {
if (tracingEnabled) {
Trace.traceBegin(Trace.TRACE_TAG_ALWAYS, getClass().getName() + ":" + code);
}
res = onTransact(code, data, reply, flags);
} catch (RemoteException|RuntimeException e) {
if (LOG_RUNTIME_EXCEPTION) {
Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e);
}
if ((flags & FLAG_ONEWAY) != 0) {
if (e instanceof RemoteException) {
Log.w(TAG, "Binder call failed.", e);
} else {
Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e);
}
} else {
reply.setDataPosition(0);
reply.writeException(e);
}
res = true;
} finally {
if (tracingEnabled) {
Trace.traceEnd(Trace.TRACE_TAG_ALWAYS);
}
}
checkParcel(this, code, reply, "Unreasonably large binder reply buffer");
reply.recycle();
data.recycle();// Just in case -- we are done with the IPC, so there should be no more strict
// mode violations that have gathered for this thread. Either they have been
// parceled and are now in transport off to the caller, or we are returning back
// to the main transaction loop to wait for another incoming transaction. Either
// way, strict mode begone!
StrictMode.clearGatheredViolations();
binderCallsStats.callEnded(callSession);return res;
}
}
最终找到异常日志:Binder call failed。Binder机制可以移步:https://www.cnblogs.com/everhad/p/6246551.html
到这里,我们找到了异常所在,问题还是没有解决,通过 RemoteException异常,联想到在getPluginHost这一步时,有两个IPluginHost 可以选择,一个是local,一个是Remote,通过阅读Replugin的介绍,Replugin默认是以“常驻进程”作为“插件管理进程”。如需切换到以“主进程”作为“插件管理进程”(也即不产生额外进程),则需要在宿主的app/build.gradle中添加下列内容,用以设置 persistentEnable 字段为False
{
apply plugin: 'replugin-host-gradle'
repluginHostConfig {
// ... 其它RePlugin参数
// 设置为“不需要常驻进程”
persistentEnable = false
}
此时,app运行进程中就不会出现GuardService进程。这时尝试install外置插件,成功了。。。
问题是暂时解决了,但是问题的根本原因还有待继续分析。