RePlugin
为什么拿出RePlugin呢?因为在我看来RePlugin虽然看起来都是360的,很可能和DroidPlugin相似。但是实际上RePlugin实现上在我看来打开了新世界的大门,其思路另辟蹊径。同时其思想,在本人看来又是一个另外一个层面的上的。最能让人另眼相看的有两点,首先打出了入侵最小的旗号,其次其工程的架构,将明确的区分了宿主和插件而且没有太多的代码入侵。宿主和插件双方能够单独运行。第三,灵活的使用了ContentProvider进行了跨进程的通信。第三点的技巧很值得我们学习。
让我们看看吧。这个插件化框架究竟是怎么回事。
老规矩先看用法。 先贴上github的wiki: github.com/Qihoo360/Re…
用法很简单,分为宿主和插件两块。
宿主配置
首先依赖
android {
// ATTENTION!!! Must CONFIG this to accord with Gradle's standard, and avoid some error
defaultConfig {
applicationId "com.qihoo360.replugin.sample.host"
...
}
...
}
// ATTENTION!!! Must be PLACED AFTER "android{}" to read the applicationId
apply plugin: 'replugin-host-gradle'
/**
* 配置项均为可选配置,默认无需添加
* 更多可选配置项参见replugin-host-gradle的RepluginConfig类
* 可更改配置项参见 自动生成RePluginHostConfig.java
*/
repluginHostConfig {
/**
* 是否使用 AppCompat 库
* 不需要个性化配置时,无需添加
*/
useAppCompat = true
/**
* 背景不透明的坑的数量
* 不需要个性化配置时,无需添加
*/
countNotTranslucentStandard = 6
countNotTranslucentSingleTop = 2
countNotTranslucentSingleTask = 3
countNotTranslucentSingleInstance = 2
}
dependencies {
compile 'com.qihoo360.replugin:replugin-host-lib:2.2.4'
...
}
首先我们继承RePlugin的Application
public class SampleApplication extends RePluginApplication
接着可以添加各种自定义配置
/**
* RePlugin允许提供各种“自定义”的行为,让您“无需修改源代码”,即可实现相应的功能
*/
@Override
protected RePluginConfig createConfig() {
RePluginConfig c = new RePluginConfig();
// 允许“插件使用宿主类”。默认为“关闭”
c.setUseHostClassIfNotFound(true);
// FIXME RePlugin默认会对安装的外置插件进行签名校验,这里先关掉,避免调试时出现签名错误
c.setVerifySign(!BuildConfig.DEBUG);
// 针对“安装失败”等情况来做进一步的事件处理
c.setEventCallbacks(new HostEventCallbacks(this));
// FIXME 若宿主为Release,则此处应加上您认为"合法"的插件的签名,例如,可以写上"宿主"自己的。
// RePlugin.addCertSignature("AAAAAAAAA");
// 在Art上,优化第一次loadDex的速度
// c.setOptimizeArtLoadDex(true);
return c;
}
@Override
protected RePluginCallbacks createCallbacks() {
return new HostCallbacks(this);
}
/**
* 宿主针对RePlugin的自定义行为
*/
private class HostCallbacks extends RePluginCallbacks {
private static final String TAG = "HostCallbacks";
private HostCallbacks(Context context) {
super(context);
}
@Override
public boolean onPluginNotExistsForActivity(Context context, String plugin, Intent intent, int process) {
// FIXME 当插件"没有安装"时触发此逻辑,可打开您的"下载对话框"并开始下载。
// FIXME 其中"intent"需传递到"对话框"内,这样可在下载完成后,打开这个插件的Activity
return super.onPluginNotExistsForActivity(context, plugin, intent, process);
}
/*
@Override
public PluginDexClassLoader createPluginClassLoader(PluginInfo pi, String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
String odexName = pi.makeInstalledFileName() + ".dex";
if (RePlugin.getConfig().isOptimizeArtLoadDex()) {
Dex2OatUtils.injectLoadDex(dexPath, optimizedDirectory, odexName);
}
long being = System.currentTimeMillis();
PluginDexClassLoader pluginDexClassLoader = super.createPluginClassLoader(pi, dexPath, optimizedDirectory, librarySearchPath, parent);
return pluginDexClassLoader;
}
*/
}
private class HostEventCallbacks extends RePluginEventCallbacks {
private static final String TAG = "HostEventCallbacks";
public HostEventCallbacks(Context context) {
super(context);
}
@Override
public void onInstallPluginFailed(String path, InstallResult code) {
// FIXME 当插件安装失败时触发此逻辑。您可以在此处做“打点统计”,也可以针对安装失败情况做“特殊处理”
// 大部分可以通过RePlugin.install的返回值来判断是否成功
super.onInstallPluginFailed(path, code);
}
@Override
public void onStartActivityCompleted(String plugin, String activity, boolean result) {
// FIXME 当打开Activity成功时触发此逻辑,可在这里做一些APM、打点统计等相关工作
super.onStartActivityCompleted(plugin, activity, result);
}
}
插件的配置
只需要添加配置
apply plugin: 'replugin-plugin-gradle'
dependencies {
compile 'com.qihoo360.replugin:replugin-plugin-lib:2.2.4'
...
}
从上面可以清楚,实际上RePlugin对我们的代码入侵性十分低,同时也通过依赖不同的类库来区分出了宿主和插件。
了解如何使用,我们来思考这个问题,如何才能做到入侵性最少,换句说就是反射最少的系统源码下,能够完成插件的类与资源的读取。
当时我看到这个RePlugin的宣言时候,也在好奇怎么样才能做到最小的入侵性。确实如果真的要实现一个Activity的跳转,我一开始给插件化的基础模型是一个Activity跳转必备的流程,当时确实无法想到更加简单的办法。
反过来思考,在阅读了这么多的插件化源码,变化的永远是我们对不同版本以及对Android源码的理解而进行反射,让系统帮忙完成资源和类的加载。不变的是,我们永远需要通过ClassLoader来查找类。从变化中找不变,难道是对ClassLoader进行处理?在ClassLoader的loadClass的时候找我们想要的类,加载插件?很大胆的想法,而这种大胆的想法,还真的给RePlugin的团队实现了。
不得不说一声,一年前我学习DroidPlugin的时候为这个团队对源码的熟悉程度献上了膝盖。而现在对源码还算熟悉的我,再一次为RePlugin团队的极具开创性的想法再度献上膝盖。看了这么开源库能让我读着,读着就跳起来的,也就这两个库。
##Host-Library宿主库
###Host中的RePlugin的关键类 源码将分为宿主的host库和插件的plugin库分别分析,同时将会根据RePugin的插件服务进程与宿主进程区分来说明。在分析之前,我先对源码中几个重要的类先列出来,这里尽可能的少列出来。个人看法和大佬们的理解角度不一样,列出来的类或许不一样。
####PmBase 作为整个RePlugin的核心类之一。控制了RePlugin的初始化。保存着从包中解析解析出来的占坑信息,类加载器,插件信息,以及其他核心类的实例。换句话说就是插件管理中心
Plugin
代表着RePlugin中插件在内存中的对象,这个类如同Small一样也会解析plugin-buildin.json生成对应的Plugin,同时也会解析外部插件的信息生成Plugin类
Loader
代表着RePlugin实际加载插件数据的执行器。
PmHostSvc
这个是指RePlugin插件管理器的进程总控制器。
PluginServiceServer
是指插件管理进程服务端的服务,只要是控制插件服务端的生命周期。
PluginManagerServer
是指插件管理进程服务端的插件管理中心,主要是通过它来完成跨进程插件操作。
PluginLibraryInternalProxy
是指插件的在宿主进程中实际的操作实现类。
PluginDexClassLoader
是用于插件寻找插件内类的类加载器
RePluginClassLoader
是用于宿主的类加载器。
接下来,我将围绕这几个类来对RePlugin的初始化和启动Activity展开讨论。
RePlugin的启动
这里先给出两幅时序图以及进程初始化的图,第一幅是宿主的,第二幅是插件进程的。注意,这里的时序图只会根据关键信息给出主要流程。下面的源码分析,默认按照多进程框架进行分析。
RePlugin的宿主进程(UI进程)的启动
RePlugin的插件管理进程的启动
标红的地方就是RePlugin宿主进程和插件管理进程的分割点。在RePlugin启动的时候,就区分出了所谓的UI进程和常驻进程。UI进程也就是我们的宿主主进程,而常驻进程是指插件管理器的进程。这里对启动做进一步的划分。
通过两个进程初始化的比较,其实双方的相似度十分高,变化是从PmBase开始。那么两者之间的进程是怎么联系,UI进程是宿主可以默认启动,但是插件进程又是何时启动呢?接下来我将一一分析。提示,这里将不会对AIDL的原理进行分析,想要了解的,可以看看我csdn中对Binder的解析,或者网上也有很多优秀的文章。
实际上插件管理进程的初始化其中还有很多的细节。这里我就以Plugin为主要线索画出的建议流程图。实际上初始化的核心模块几乎都在PmBase中完成。所以我们其实可以先去PmBase中看看初始化的init的方法。
###UI进程的初始化
void init() {
RePlugin.getConfig().getCallbacks().initPnPluginOverride();
if (HostConfigHelper.PERSISTENT_ENABLE) {
// (默认)“常驻进程”作为插件管理进程,则常驻进程作为Server,其余进程作为Client
if (IPC.isPersistentProcess()) {
// 初始化“Server”所做工作
initForServer();
} else {
// 连接到Server
initForClient();
}
} else {
// “UI进程”作为插件管理进程(唯一进程),则UI进程既可以作为Server也可以作为Client
...
}
// 最新快照
PluginTable.initPlugins(mPlugins);
// 输出
...
}
开始的时候默认HostConfigHelper.PERSISTENT_ENABLE为打开,允许使用插件进程来维护插件信息。刚开始我们通过Application的attachBaseContext进来的,也就是说此时一定是UI进程,那么一定会走下面的initForClient方法的分支。
从注释可以清楚,常驻进程为插件管理进程,其余的如插件和宿主统统都是客户端进程。
initForClient
/**
* Client(UI进程)的初始化
*
*/
private final void initForClient() {
// 1. 先尝试连接
PluginProcessMain.connectToHostSvc();
// 2. 然后从常驻进程获取插件列表
refreshPluginsFromHostSvc();
}
第一个方法是核心。当前作为UI进程会尝试的连接常驻进程。
/**
* 非常驻进程调用,获取常驻进程的 IPluginHost
*/
static final void connectToHostSvc() {
Context context = PMF.getApplicationContext();
//
IBinder binder = PluginProviderStub.proxyFetchHostBinder(context);
if (binder == null) {
// 无法连接到常驻进程,当前进程自杀
System.exit(1);
}
//
try {
binder.linkToDeath(new IBinder.DeathRecipient() {
@Override
public void binderDied() {
// 检测到常驻进程退出,插件进程自杀
if (PluginManager.isPluginProcess()) {
System.exit(0);
}
sPluginHostRemote = null;
// 断开和插件化管理器服务端的连接,因为已经失效
PluginManagerProxy.disconnect();
}
}, 0);
} catch (RemoteException e) {
// 无法连接到常驻进程,当前进程自杀
System.exit(1);
}
//
sPluginHostRemote = IPluginHost.Stub.asInterface(binder);
// 连接到插件化管理器的服务端
// Added by Jiongxuan Zhang
try {
PluginManagerProxy.connectToServer(sPluginHostRemote);
// 将当前进程的"正在运行"列表和常驻做同步
// TODO 若常驻进程重启,则应在启动时发送广播,各存活着的进程调用该方法来同步
PluginManagerProxy.syncRunningPlugins();
} catch (RemoteException e) {
// 获取PluginManagerServer时出现问题,可能常驻进程突然挂掉等,当前进程自杀
System.exit(1);
}
// 注册该进程信息到“插件管理进程”中
PMF.sPluginMgr.attach();
}
这个方法做两个事情,第一,尝试着通过ContentProvider来查找有没有连接插件进程的PmHostSvc这个插件进程的总控制器的aidl的IBinder。
/**
* @param context
* @param selection
* @return
*/
private static final IBinder proxyFetchHostBinder(Context context, String selection) {
//
Cursor cursor = null;
try {
Uri uri = ProcessPitProviderPersist.URI;
cursor = context.getContentResolver().query(uri, PROJECTION_MAIN, selection, null, null);
if (cursor == null) {
return null;
}
while (cursor.moveToNext()) {
//
}
IBinder binder = BinderCursor.getBinder(cursor);
return binder;
} finally {
CloseableUtils.closeQuietly(cursor);
}
}
第二,尝试的连接着插件进程的服务,通过上面给予的IBinder,这个IBinder指代的是常驻进程的远程端的代理,换句话说就是通过调用常驻进程调用fetchManagerServer,获取常驻进程的插件服务。
/**
* 连接到常驻进程,并缓存IPluginManagerServer对象
*
* @param host IPluginHost对象
* @throws RemoteException 和常驻进程通讯出现异常
*/
public static void connectToServer(IPluginHost host) throws RemoteException {
if (sRemote != null) {
return;
}
sRemote = host.fetchManagerServer();
}
是怎么连到插件进程的,这里先埋个伏笔。先假设我们都连接成功了。
refreshPluginsFromHostSvc
从PmHostSvc中刷新插件数据,PmHostSvc现在这里说明了,由于实现了IPluginHost.Stub,所以实际上就是常驻进程的总控制器。
private void refreshPluginsFromHostSvc() {
List<PluginInfo> plugins = null;
try {
plugins = PluginProcessMain.getPluginHost().listPlugins();
} catch (Throwable e) {
}
// 判断是否有需要更新的插件
// FIXME 执行此操作前,判断下当前插件的运行进程,具体可以限制仅允许该插件运行在一个进程且为自身进程中
List<PluginInfo> updatedPlugins = null;
if (isNeedToUpdate(plugins)) {
try {
updatedPlugins = PluginManagerProxy.updateAllPlugins();
} catch (RemoteException e) {
e.printStackTrace();
}
}
if (updatedPlugins != null) {
refreshPluginMap(updatedPlugins);
} else {
refreshPluginMap(plugins);
}
}
做了两件事情,先从PluginProcessMain获取常驻进程总控制器的远程代理。读取常驻进程中需要加载的插件信息列表。一旦发现我们有需要加载插件,则立即调用updateAllPlugins,来控制常驻进程的PluginManagerServer来加载安装进来的插件,最后再同步到UI进程的mPluginsList集合中。
PatchClassLoaderUtils.patch
接着也是RePlugin的核心之一,为宿主创造了宿主的ClassLoader,也是整个RePlugin体系唯一Hook的地方。
public static boolean patch(Application application) {
try {
// 获取Application的BaseContext (来自ContextWrapper)
Context oBase = application.getBaseContext();
...
ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");
if (oClassLoader == null) {
return false;
}
// 外界可自定义ClassLoader的实现,但一定要基于RePluginClassLoader类
ClassLoader cl = RePlugin.getConfig().getCallbacks().createClassLoader(oClassLoader.getParent(), oClassLoader);
// 将新的ClassLoader写入mPackageInfo.mClassLoader
ReflectUtils.writeField(oPackageInfo, "mClassLoader", cl);
// 设置线程上下文中的ClassLoader为RePluginClassLoader
// 防止在个别Java库用到了Thread.currentThread().getContextClassLoader()时,“用了原来的PathClassLoader”,或为空指针
Thread.currentThread().setContextClassLoader(cl);
} catch (Throwable e) {
e.printStackTrace();
return false;
}
return true;
}
主要的想法是自己创造一个ClassLoader来替代掉Android系统中用来寻找类的类加载器。根据上面插件化基础框架中可以得知,所有的类查找都是通过获取LoadedAPk中的ClassLoader来查找类。那么这段话的意思就很简单了,就是替换掉系统的ClassLoader。让我们看看Applicaion中Context的mPackageInfo究竟是不是LoadedApk吧。Context的实现类是ContextImpl,我们直接看看里面是什么
final LoadedApk mPackageInfo;
确实思路是正确的,这里埋下第二个伏笔,将会在Activity的启动中让我们聊聊这个创建的RePluginClassLoader究竟在整个RePlugin起了什么作用。
callAttach
根据我们的时序图初始化接下来会走到callAttach这个方法,我们看看
final void callAttach() {
//
mClassLoader = PmBase.class.getClassLoader();
// 挂载
for (Plugin p : mPlugins.values()) {
p.attach(mContext, mClassLoader, mLocal);
}
// 加载默认插件
if (PluginManager.isPluginProcess()) {
if (!TextUtils.isEmpty(mDefaultPluginName)) {
//
Plugin p = mPlugins.get(mDefaultPluginName);
if (p != null) {
boolean rc = p.load(Plugin.LOAD_APP, true);
if (!rc) {
}
if (rc) {
mDefaultPlugin = p;
mClient.init(p);
}
}
}
}
}
下面部分是核心,如果是插件则会从Plugin中解析一次插件的信息,这里先不谈,到了常驻进程的时候会详细说说这个Plugin.load方法。
###onCreate
final void callAppCreate() {
// 计算/获取cookie
if (IPC.isPersistentProcess()) {
mLocalCookie = PluginProcessMain.getPersistentCookie();
} else {
...
if (!IPC.isPersistentProcess()) {
// 由于常驻进程已经在内部做了相关的处理,此处仅需要在UI进程注册并更新即可
registerReceiverAction(ACTION_NEW_PLUGIN);
registerReceiverAction(ACTION_UNINSTALL_PLUGIN);
}
}
这里则是注册了插件的安装和卸载的监听。
所以,先不论RePlugin的RePluginClassLoader究竟做了什么。实际上,如果启用了多进程框架的RePlugin的管理模式,其实插件的解析和加载都是在常驻进程中完成,而UI进程只是做一次插件信息的同步处理。
RePlugin从UI进程启动常驻进程
还记得我上面的第一个伏笔吧。现在我们回到宿主在连接常驻进程的方法,proxyFetchHostBinder。
//selection
private static final String SELECTION_MAIN_BINDER = "main_binder";
private static final String PROJECTION_MAIN[] = {
"main"
};
private static final String AUTHORITY_PREFIX = IPC.getPackageName() + ".loader.p.main";
//ProcessPitProviderPersist.URI
public static final Uri URI = Uri.parse("content://" + AUTHORITY_PREFIX + "/main");
private static final IBinder proxyFetchHostBinder(Context context, String selection) {
//
Cursor cursor = null;
try {
Uri uri = ProcessPitProviderPersist.URI;
cursor = context.getContentResolver().query(uri, PROJECTION_MAIN, selection, null, null);
还记得ContentProvider的吧。这是内容提供器,因为开发中用的不多,我都几乎都忘记这个Android的四大组件之一的原理,但是用法还是记得的。
首先需要拿到和AndroidManifest中注册的权限和这里对应,找找看注册在注册文件中对应的内容提供器有什么猫腻。
<provider android:name='com.qihoo360.replugin.component.process.ProcessPitProviderPersist'
android:authorities='com.qihoo360.replugin.sample.host.loader.p.main'
android:exported='false'
android:process=':GuardService' />
你会发现这个用来注册在AndroidManifest的内容提供器,是位于GuardService进程的。从这里我们得知,我们在调用getContentResolver().query的方法,从这个GuardService进程中的内容提供器获取我们想要的IBinder。
这里我们可以进一步的猜测,整个常驻进程是不是就是指GuardService呢?
####ContentProvider跨进程启动 这里我们就需要分析一下四大组件ContentProvider的源码了。 在整个App启动进程的时候,会从Zygote.cpp中fork一个新的进程出来,目标类是AppThread。换句话说就是,main方法为进程第一个运行的方法。
如果不熟悉源码的可以根据我上面给的时序图读一遍源码。这里我稍微解释一下,每一次在进程启动的时候,都会绑定一次Application。加载ContentProvider的时期,这个时候会创建好Instrumentation,并且在makeApplication之后,Instrumentation的onCreate之前。换句话说就是在Application的attchBaseContext和onCreate之后。
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
// app's custom Application class
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
installContentProviders(app, data.providers);
mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
}
}
try {
mInstrumentation.onCreate(data.instrumentationArgs);
}
catch (Exception e) {
throw new RuntimeException(
"Exception thrown in onCreate() of "
+ data.instrumentationName + ": " + e.toString(), e);
}
这里多说一句也很重要,你自己往源码深处查看的时候,你会发现实际上这个加载的内容提供器,实际上会从PMS中解析的数据找出和当前进程名一致的内容提供器,而不一致的会被筛选掉。
我们可以看到PMS中这段源码
@Override
public @NonNull ParceledListSlice<ProviderInfo> queryContentProviders(String processName,
int uid, int flags) {
final int userId = processName != null ? UserHandle.getUserId(uid)
: UserHandle.getCallingUserId();
if (!sUserManager.exists(userId)) return ParceledListSlice.emptyList();
flags = updateFlagsForComponent(flags, userId, processName);
ArrayList<ProviderInfo> finalList = null;
// reader
synchronized (mPackages) {
final Iterator<PackageParser.Provider> i = mProviders.mProviders.values().iterator();
while (i.hasNext()) {
final PackageParser.Provider p = i.next();
PackageSetting ps = mSettings.mPackages.get(p.owner.packageName);
if (ps != null && p.info.authority != null
&& (processName == null
|| (p.info.processName.equals(processName)
&& UserHandle.isSameApp(p.info.applicationInfo.uid, uid)))
&& mSettings.isEnabledAndMatchLPr(p.info, flags, userId)) {
if (finalList == null) {
finalList = new ArrayList<ProviderInfo>(3);
}
ProviderInfo info = PackageParser.generateProviderInfo(p, flags,
ps.readUserState(userId), userId);
if (info != null) {
finalList.add(info);
}
}
}
}
if (finalList != null) {
Collections.sort(finalList, mProviderInitOrderSorter);
return new ParceledListSlice<ProviderInfo>(finalList);
}
return ParceledListSlice.emptyList();
}
结合源码解析出来的结果,以及RePlugin的源码。我们可以很清楚的明白,以下两点:
- 实际上当我们第一次加载ContentProvider的时候是没有标记为GuardService进程的内容提供器。必须是通过我们ContentProvider.query的操作调起我们的常驻进程。
context.getContentResolver().query(uri, PROJECTION_MAIN, selection, null, null);
这段代码可说是UI进程初始化的核心。负担了两个角色,第一调起常驻进程,第二获取常驻进程的总控制器的远程代理。
- 当我们重新拉起进程的时候,会重新走一边Application的初始化,也就是说会再走一次我们RePlugin的初始化代码,不同的是这一次是常驻进程,所以将会走到了不同的分支。
RePlugin的常驻进程的启动
通过ContentProvider的源码的阅读,也就能够明白为什么顺序是上面的时序图样子。
还是一样我们直奔PmBase的initForServer
initForServer
private final void initForServer() {
mHostSvc = new PmHostSvc(mContext, this);
PluginProcessMain.installHost(mHostSvc);
PluginProcessMain.schedulePluginProcessLoop(PluginProcessMain.CHECK_STAGE1_DELAY);
// 兼容即将废弃的p-n方案 by Jiongxuan Zhang
mAll = new Builder.PxAll();
Builder.builder(mContext, mAll);
refreshPluginMap(mAll.getPlugins());
// [Newest!] 使用全新的RePlugin APK方案
// Added by Jiongxuan Zhang
try {
List<PluginInfo> l = PluginManagerProxy.load();
if (l != null) {
refreshPluginMap(l);
}
} catch (RemoteException e) {
}
}
做了几件事情,这里分别实例化了PmHostSvc,这个服务端的总控制器,以及内部的PluginServiceServer和PluginManagerServer。
installHost
static final void installHost(IPluginHost host) {
sPluginHostLocal = host;
// 连接到插件化管理器的服务端
// Added by Jiongxuan Zhang
try {
PluginManagerProxy.connectToServer(sPluginHostLocal);
} catch (RemoteException e) {
// 基本不太可能到这里,直接打出日志
}
}
public static void connectToServer(IPluginHost host) throws RemoteException {
if (sRemote != null) {
return;
}
sRemote = host.fetchManagerServer();
}
@Override
public IPluginManagerServer fetchManagerServer() throws RemoteException {
return mManager.getService();
}
这里很有意思,我们会再一次的尝试着通过PmHostSvc去获取IPluginManagerServer对象,但是实际上我们IPluginManagerServer这个对象现在指的是PluginManagerServer,已经在在PmHostSvc中实例化出来。
PmHostSvc(Context context, PmBase packm) {
mContext = context;
mPluginMgr = packm;
mServiceMgr = new PluginServiceServer(context);
mManager = new PluginManagerServer(context);
}
虽然并不影响使用,但是按照我们多进程插件框架来说,这里的意思是在常驻进程中通过AIDL再一次的和常驻进程的服务端进行通信。相当于对着常驻进程再度切开两个接口PluginServiceServer和PluginManagerServer让外部进行跨进程通信。
这么做是为了在关闭常驻进程模式的时候,UI进程将会作为服务端和客服端,让其他插件的进程链接进来。
Builder.builder
static final void builder(Context context, PxAll all) {
// 搜索所有本地插件和V5插件
Finder.search(context, all);
// 删除不适配的PLUGINs
for (PluginInfo p : all.getOthers()) {
// TODO 如果已存在built-in和V5则不删除
boolean rc = p.deleteObsolote(context);
if (!rc) {
}
}
// 删除所有和PLUGINs不一致的DEX文件
deleteUnknownDexs(context, all);
// 删除所有和PLUGINs不一致的SO库目录
// Added by Jiongxuan Zhang
deleteUnknownLibs(context, all);
// 构建数据
}
这个方法中,我们着重看看Finder.search(context, all);扫描所有的插件数据并且转化为Plugin类
/**
* 扫描插件
*/
static final void search(Context context, PxAll all) {
// 扫描内置插件
FinderBuiltin.loadPlugins(context, all);
// 扫描V5插件
File pluginDir = context.getDir(Constant.LOCAL_PLUGIN_SUB_DIR, 0);
V5Finder.search(context, pluginDir, all);
// 扫描现有插件,包括刚才从V5插件文件更新过来的文件
HashSet<File> deleted = new HashSet<File>();
{
searchLocalPlugins(pluginDir, all, deleted);
}
// 删除非插件文件和坏的文件
for (File f : deleted) {
boolean rc = f.delete();
if (!rc) {
}
}
deleted.clear();
}
让我们先看看内置插件的逻辑
static final void loadPlugins(Context context, PxAll all) {
InputStream in;
// 读取内部配置
in = null;
try {
in = context.getAssets().open("plugins-builtin.json");
readConfig(in, all);
} catch (FileNotFoundException e0) {
} catch (Throwable e) {
}
CloseableUtils.closeQuietly(in);
}
有点意思的地方是,这里实际上和Small的思路很像。也是通过读取Asset文件夹下面的plugins-builtin.json文件来获取插件信息。这个json文件并不需要我们自己编写实际上会通过host-gradle的gradle插件自己生成的。最后把数据保存到PxAll缓存中。
我的gradle插件这部分不太熟悉,但是好在gradle插件的语法简单,我们可以简单的明白这个json是通过读取插件中AndroidManifest里面的内容,生成的json文件
public PluginInfoParser(File pluginFile, def config) {
pluginInfo = new PluginInfo()
ApkFile apkFile = new ApkFile(pluginFile)
String manifestXmlStr = apkFile.getManifestXml()
ByteArrayInputStream inputStream = new ByteArrayInputStream(manifestXmlStr.getBytes("UTF-8"))
SAXParserFactory factory = SAXParserFactory.newInstance()
SAXParser parser = factory.newSAXParser()
parser.parse(inputStream, this)
String fullName = pluginFile.name
pluginInfo.path = config.pluginDir + "/" + fullName
String postfix = config.pluginFilePostfix
pluginInfo.name = fullName.substring(0, fullName.length() - postfix.length())
}
plugins-builtin.json里面的json是这样的。包含了插件的包名,路径名,版本以及其他信息。
{"high":null,"frm":null,"ver":104,"low":null,"pkg":"com.qihoo360.replugin.sample.demo1","path":"plugins/demo1.jar","name":"demo1"}
通过这些初步的生成了在Asset文件夹中内置的插件信息。和Small不同的,Small的build.json除了可以制定插件名还可以制定模块名以及相应的规则。
我们在看看所谓的V5插件也就是不存在Asset的外部插件,当然也有目录限制,就在“plugins_v3”这里,这里一般是指从外部下载进来的插件。
static final void search(Context context, File pluginDir, PxAll all) {
// 扫描V5下载目录
ArrayList<V5FileInfo> v5Plugins = new ArrayList<V5FileInfo>();
{
File dir = RePlugin.getConfig().getPnInstallDir();
searchV5Plugins(dir, v5Plugins);
}
// 同步V5原始插件文件到插件目录
for (V5FileInfo p : v5Plugins) {
ProcessLocker lock = new ProcessLocker(RePluginInternal.getAppContext(), p.mFile.getParent(), p.mFile.getName() + ".lock");
if (lock.isLocked()) {
// 插件文件不可用,直接跳过
continue;
}
PluginInfo info = p.updateV5FileTo(context, pluginDir, false, true);
// 已检查版本
if (info == null) {
} else {
all.addV5(info);
}
}
}
这里的思路会筛选出能够使用的下载哈见接着再更新到“plugins_v3”这个目录中,并且把数据保存到PxAll缓存中。这个下载路径我们可以通过RePlugin初始化配置下调配,这里就不多说了。
searchLocalPlugins(pluginDir, all, deleted);
这一句将会做一次对加载进来的信息,做一次筛选,找出是否版本号不一致需要更新的,是否有最新的需要更新等。
PluginManagerProxy.load();
我们再看看
PluginManagerProxy.load();
这个方法实际上调用的是PluginManagerServer远程端的load方法。我们看看load方法。
@Override
public List<PluginInfo> load() throws RemoteException {
synchronized (LOCKER) {
return PluginManagerServer.this.loadLocked();
}
}
private List<PluginInfo> loadLocked() {
if (!mList.load(mContext)) {
return null;
}
// 执行“更新或删除Pending”插件,并返回结果
return updateAllLocked();
}
public boolean load(Context context) {
try {
// 1. 新建或打开文件
File d = context.getDir(Constant.LOCAL_PLUGIN_APK_SUB_DIR, 0);
File f = new File(d, "p.l");
if (!f.exists()) {
// 不存在?直接创建一个新的即可
if (!f.createNewFile()) {
return false;
} else {
return true;
}
}
// 2. 读出字符串
String result = FileUtils.readFileToString(f, Charsets.UTF_8);
if (TextUtils.isEmpty(result)) {
return false;
}
// 3. 解析出JSON
mJson = new JSONArray(result);
} catch (IOException e) {
return false;
} catch (JSONException e) {
return false;
}
for (int i = 0; i < mJson.length(); i++) {
JSONObject jo = mJson.optJSONObject(i);
if (jo != null) {
PluginInfo pi = PluginInfo.createByJO(jo);
if (pi == null) {
continue;
}
addToMap(pi);
}
}
return true;
}
这里是指RePlugin将会检测通过install进来的安装进来的apk插件的安装信息,读取其中的json数据,再更新插件列表信息,同时生成PluginInfo这个外部插件的信息,并且更新到之前的json数据文件中。
通过这种方式,RePlugin控制了宿主内的插件,下载的V5插件以及安装进来的apk插件。
refreshPluginMap
private final void refreshPluginMap(List<PluginInfo> plugins) {
if (plugins == null) {
return;
}
for (PluginInfo info : plugins) {
Plugin plugin = Plugin.build(info);
putPluginObject(info, plugin);
}
}
private void putPluginObject(PluginInfo info, Plugin plugin) {
if (mPlugins.containsKey(info.getAlias()) || mPlugins.containsKey(info.getPackageName())) {
// 找到已经存在的
Plugin existedPlugin = mPlugins.get(info.getPackageName());
if (existedPlugin == null) {
existedPlugin = mPlugins.get(info.getAlias());
}
if (existedPlugin.mInfo.getVersion() < info.getVersion()) {
// 同时加入PackageName和Alias(如有)
mPlugins.put(info.getPackageName(), plugin);
if (!TextUtils.isEmpty(info.getAlias())) {
// 即便Alias和包名相同也可以再Put一次,反正只是覆盖了相同Value而已
mPlugins.put(info.getAlias(), plugin);
}
} else {
}
} else {
// 同时加入PackageName和Alias(如有)
mPlugins.put(info.getPackageName(), plugin);
if (!TextUtils.isEmpty(info.getAlias())) {
// 即便Alias和包名相同也可以再Put一次,反正只是覆盖了相同Value而已
mPlugins.put(info.getAlias(), plugin);
}
}
}
此时,无论是UI还是常驻进程将会通过refreshPluginMap通过PluginManagerServer生成的消息把安装的apk插件同步到内存中。
callAttach
由于不是插件进程,所以并没有在意的地方。时序图后面的将会在Activity启动拿出来详细。
总结:初始化,实际上做的工作主要有两点: 第一,连接常驻进程,并且初始化相关的工作,如ClassLoader的替换等 第二,通过常驻进程解析插件信息,并且同步到UI进程。