360在开源了DroidPlugin后又开源了RePlugin(从研发时间上是RePlugin更早),这不禁让人想问两者的区别。从RePlugin的github中的wiki我们可以找到答案,其最大区别主要是稳定性和宿主与插件的交互。
稳定性:
DroidPlugin hook了多处系统api,而系统api版本的差异性,以及手机厂商可能的修改导致需要做非常多的兼容性处理工作。RePlugin只hook了ClassLoader一处,相比之下稳定性要高很多。
DroidPlugin 也因此可以做到不修改插件原有代码情况下直接加载任意apk,RePlugin不能直接把任意apk当插件加载,需要引入Replugin的编译插件(Replugin 里面插件的startActivity,getResource等都是通过让插件使用PluginContext来完成的,而这个过程是通过gradle 自动化将插件的Activity等继承PluginActivity完成的。
宿主与插件的交互:
RePlugin宿主与插件有交互,DroidPlugin是独立。DroidPlugin侧重于加载第三方独立插件,比如微信,并且插件不能访问宿主的代码和资源。RePlugin插件可以访问代码,且通过间接方式场景进行资源交互https://github.com/Qihoo360/RePlugin/wiki/FAQ
本文主要从三个场景切入,通过分析代码(版本:replugin 2.3.0)来了解RePlugin
1、宿主进程初始化
2、插件启动过程
3、跳转插件Activity流程
使用上,宿主通过继承RePluginApplication完成插件框架的注册,因此RePluginApplication作为分析的入口
先通过时序图看下宿主进程初始化的流程
RePluginApplication.attachBaseContext:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//配置初始化,比如插件安装的路径,是否开启插件签名校验等等
RePluginConfig c = createConfig();
if (c == null) {
c = new RePluginConfig();
}
//插件框架对外回调接口集,比如创建宿主用的RePluginClassLoader,插件用的PluginDexClassLoader
RePluginCallbacks cb = createCallbacks();
if (cb != null) {
c.setCallbacks(cb);
}
RePlugin.App.attachBaseContext(this, c);
}
RePlugin.App.attachBaseContext:
public static void attachBaseContext(Application app, RePluginConfig config) {
......
//配置没有设置的话使用默认配置
sConfig.initDefaults(app);
//设置常驻进程的进程名字
IPC.init(app);
// 初始化HostConfigHelper(通过反射HostConfig来实现)
// NOTE 一定要在IPC类初始化之后才使用
HostConfigHelper.init();
PMF.init(app);
// 执行插件挂载(绑定关键变量)和加载默认插件
PMF.callAttach();
}
PMF.init
public static final void init(Application application) {
//对这个app根据所在的进程获取用户id,和进程索引。映射appliacation和进程的关系
PluginManager.init(application);
//PmBase 是插件管理的核心类,进程的启动,插件的启动等相关,四大组件的查询都在这里。
(1) sPluginMgr = new PmBase(application);
(2)sPluginMgr.init();
(3)PatchClassLoaderUtils.patch(application);
}
(1)PmBase(Context context)
PmBase(Context context) {
if (PluginManager.sPluginProcessIndex == IPluginManager.PROCESS_UI || PluginManager.isPluginProcess()) {
//通过进程索引标示四大组件的坑位,后面查询的时候使用(对应通过gradle插件在mainfest生成的坑位,
//可以看到每个进程的坑位是有限的。因为这些坑位是在跨插件调用才会使用的)
mContainerProviders.add(IPC.getPackageName() + CONTAINER_PROVIDER_PART + suffix);
mContainerServices.add(IPC.getPackageName() + CONTAINER_SERVICE_PART + suffix);
}
//PluginProcessPer可以看作是提供服务的类,client通过binder传给常驻进程,然后常驻进程收到
// 请求时分发到对应的进程
mClient = new PluginProcessPer(context, this, PluginManager.sPluginProcessIndex, mContainerActivities);
//PluginCommImpl主要处理宿主与插件,插件与插件之间的通信(更像是请求接口)。
mLocal = new PluginCommImpl(context, this);
//框架内部调用,startActivity等方法
mInternal = new PluginLibraryInternalProxy(this);
}
(2)PmBase类 sPluginMgr.init()
void init() {
//(默认)“常驻进程”作为插件管理进程,省略了ui进程作为插件管理进程的情况
if (HostConfigHelper.PERSISTENT_ENABLE) {
// 常驻进程作为Server,其余进程作为Client。
// 时间线是宿主启动,第一次走到这里,显然当前进程是client。走initForClient,在里面
// 会去启动常驻进程。并等待常驻进程。然后常驻进程会走一遍上面的流程。然后在这里initForServer
//常驻进程的application所有流程走完了才会继续client的原有流程下去。
if (IPC.isPersistentProcess()) {
// 初始化“Server”所做工作
② initForServer();
} else {
// 连接到Server
① initForClient();
}
//初始化本进程下的插件
③PluginTable.initPlugins(mPlugins);
}
① PmBase.initForClient()
private final void initForClient() {
// 1. 先尝试连接
PluginProcessMain.connectToHostSvc();
// 2. 然后从常驻进程获取插件列表
refreshPluginsFromHostSvc();
}
PluginProcessMain.connectToHostSvc()
static final void connectToHostSvc() {
//通过context.getContentResolver().query()启动ContentProvider指定了常驻进程。
//这里使用ContentProvider,是因为启动目标进程并完成application的初始化是同步的,确保application初始化完成
// 调用getContentResolver()同步等待AMS获取ContentResolver,contentProvider进程没起来
// AMS会wait等待ContentProvider所在进程ActivityThread的application 初始化完后安装contentProvider,
// 发布到AMS,调用notify 等待的线程。
//启动常驻进程后做完常驻进程的初始化后在query获得的cursor
// IBinder binder = BinderCursor.getBinder(cursor);获取常驻进程的binder以便后面通信
IBinder binder = PluginProviderStub.proxyFetchHostBinder(context);
// .....省略异常处理和死亡后处理的情况
//把上面的binder 转成IPluginHost 后面通信时使用
sPluginHostRemote = IPluginHost.Stub.asInterface(binder);
if (LOG) {
LogDebug.d(PLUGIN_TAG, "host binder.i = " + PluginProcessMain.sPluginHostRemote);
}
// 连接到插件化管理器的服务端
//获取插件管理器的binder通信客户端
//PluginManagerServer插件管理器:用来控制插件的安装、卸载、获取等。运行在常驻进程中
//IPluginHost:涉及到插件交互、运行机制有关的管理器
//可以把PluginManagerServer类比为framework的pms而IPluginHost为ams
PluginManagerProxy.connectToServer(sPluginHostRemote);
// 将当前进程的"正在运行"列表同步到常驻进程
// TODO 若常驻进程重启,则应在启动时发送广播,各存活着的进程调用该方法来同步
PluginManagerProxy.syncRunningPlugins();
// 注册该进程信息到“插件管理进程”中
// 把PluginProcessPer传给IPluginHost之后调用客户端进程时通信用
PMF.sPluginMgr.attach();
}
PmBase.refreshPluginsFromHostSvc( )
/**
* 从HostSvc(插件管理所在进程)获取所有的插件信息
*/
private void refreshPluginsFromHostSvc() {
//在initForServer 的Builder.builder()会获取插件信息然后加载进到HostSvc(IPluginHost的服务端)
// 获取插件信息
List plugins = PluginProcessMain.getPluginHost().listPlugins();
// 判断是否有需要更新的插件
// FIXME 执行此操作前,判断下当前插件的运行进程,具体可以限制仅允许该插件运行在一个进程且为自身进程中
List updatedPlugins = null;
if (isNeedToUpdate(plugins)) {
// 下载新下发的插件可以更新。。。通过PluginManagerServer更新插件,类似pms更新应用
updatedPlugins = PluginManagerProxy.updateAllPlugins();
}
// 更新本地插件信息
if (updatedPlugins != null) {
refreshPluginMap(updatedPlugins);
} else {
refreshPluginMap(plugins);
}
}
② initForServer() 常驻进程初始化
private final void initForServer() {
// 初始化PmHostSvc和PluginManagerServer和PluginServiceServer 三个主要服务
// 并将 PmHostSvc绑定到PluginManagerServer
mHostSvc = new PmHostSvc(mContext, this);
PluginProcessMain.installHost(mHostSvc);
PluginProcessMain.schedulePluginProcessLoop(PluginProcessMain.CHECK_STAGE1_DELAY);
// 旧方案:
//通过Builder扫描插件,并更新PluginInfo
// 兼容即将废弃的p-n方案 by Jiongxuan Zhang
mAll = new Builder.PxAll();
Builder.builder(mContext, mAll);
refreshPluginMap(mAll.getPlugins());
// [Newest!] 使用全新的RePlugin APK方案
// Added by Jiongxuan Zhang
List l = PluginManagerProxy.load();
if (l != null) {
// 将"纯APK"插件信息并入总的插件信息表中,方便查询
// 这里有可能会覆盖之前在p-n中加入的信息。本来我们就想这么干,以"纯APK"插件为准
refreshPluginMap(l);
}
关于插件的信息
外置插件(未来将只有这一种目录):
- APK存放路径:主程序路径/app_p_a
- Dex存放路径:主程序路径/app_p_od
- Native存放路径:主程序路径/app_p_n
- 插件数据存放路径:主程序路径/app_plugin_v3_data
内置插件 & 旧P-N插件(未来和等同于外置插件):
- APK存放路径:主程序路径/app_plugin_v3
- Dex存放路径:主程序路径/app_plugin_v3_odex
- Native存放路径:主程序路径/app_plugin_v3_libs
- 插件数据存放路径:主程序路径/app_plugin_v3_data
文件的组织形式
外置插件:为了方便使用,插件会有一个JSON文件,用来记录所有已安装插件的信息。目前位于“主程序路径/app_p_a/p.l”中。有兴趣的朋友可以自行打开此文件来阅览其中内容。
安装完后会在/data/data/packageName/files/app_p_a/p.l 记下插件的信息
内置插件:不同于外置插件,内置插件的JSON文件只存放于主程序“assets/plugins-builtin.json”文件下。每次会从那里获取信息。
可以看官网的介绍https://github.com/Qihoo360/RePlugin/wiki/%E6%8F%92%E4%BB%B6%E7%9A%84%E7%AE%A1%E7%90%86
③PluginTable.initPlugins(mPlugins)
刷新插件表,支持以包名和别名为key的插件
static final void initPlugins(Map plugins) {
synchronized (PLUGINS) {
for (Plugin plugin : plugins.values()) {
putPluginInfo(plugin.mInfo);
}
}
}
private static void putPluginInfo(PluginInfo info) {
// 同时加入PackageName和Alias(如有)
PLUGINS.put(info.getPackageName(), info);
if (!TextUtils.isEmpty(info.getAlias())) {
// 即便Alias和包名相同也可以再Put一次,反正只是覆盖了相同Value而已
PLUGINS.put(info.getAlias(), info);
}
}
回到PMF.init的(3)PatchClassLoaderUtils.patch(application)
public static boolean patch(Application application) {
// 获取Application的BaseContext (来自ContextWrapper)
Context oBase = application.getBaseContext();
// 获取mBase.mPackageInfo
// 1. ApplicationContext - Android 2.1
// 2. ContextImpl - Android 2.2 and higher
// 3. AppContextImpl - Android 2.2 and higher
Object oPackageInfo = ReflectUtils.readField(oBase, "mPackageInfo");
// mPackageInfo的类型主要有两种:
// 1. android.app.ActivityThread$PackageInfo - Android 2.1 - 2.3
// 2. android.app.LoadedApk - Android 2.3.3 and higher
// 获取mPackageInfo.mClassLoader
ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");
// 外界可自定义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);
return true;
}
对照系统设置ClassLoader绑定到Context比较好理解,以下是andoird p的源码
application:ActivityThread.handleBindApplication
private void handleBindApplication(AppBindData data) {
....
// 这里创建LoadedApk实例并放进缓存,后面创建Activity会取出这个LoadedApk
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
....
}
LoadedApk.makeApplication
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
Application app = null;
//mainfest我们设置的自定义application
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
//实际调用createOrUpdateClassLoaderLocked创建ClassLoader,把应用下的jar,dex,so路径加到
//ClassLoader,后面类加载用。非系统应用默认是获取pathClassLoader
//后面创建插件的ClassLoader过程可以和这个进行对比
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
//Thread.currentThread().setContextClassLoader(contextClassLoader);
initializeJavaContextClassLoader();
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
//反射创建application,并且调用attach 最后调用application.attachBaseContext 把appContext
//设置到mBase变量
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
return app;
}
代码涉及的类关系如图所示
另外hook ClassLoader的方法参考术哥对droidPulgin和small的讲解
droidPlugin是通过hook掉LoadedApk来达到hook ClassLoader,因为getPackageInfoNoCheck是public的
所以能够保障其稳定性
small是通过给默认ClassLoader添加补丁的方式
http://weishu.me/2016/04/05/understand-plugin-framework-classloader/
参考:
插件的管理:https://github.com/Qihoo360/RePlugin/wiki/%E6%8F%92%E4%BB%B6%E7%9A%84%E7%AE%A1%E7%90%86
Android 插件化原理解析——插件加载机制:
http://weishu.me/2016/04/05/understand-plugin-framework-classloader/
Replugin 全面解析(1):https://www.jianshu.com/p/5994c2db1557
RePlugin中如何打开插件中的自定义进程Activity:https://mp.weixin.qq.com/s/IpNcyTjML16og4LrxjxFmQ
官方wiki :https://github.com/Qihoo360/RePlugin/wiki