1.replugin-host-gralde: 宿主脚本
2.replugin-host-library: 宿主库
3.replugin-plugin-gradle: 插件脚本
4.replugin-plugin-library:插件库
打算从以上四个模块对replugin进行拆解阅读
RePlugin的思想是Hook ClassLoader, 因此接下来就围绕ClassLoader进行分析
1.1 插件的安装
1.2 内置插件
1.3 外置插件
1.4 replugin-host-gradle
1.5 replugin-host-library
1.6 replugin-plugin-gradle
1.7 replugin-plugin-library
RePlugin插件的安装并不会真正的处理插件APK中的dex、so库、资源等, 只是将插件移动到需要的位置, 这个位置默认是宿主的context.getFilesDir(), 然后将插件信息包装成Plugin对象并绑定宿主的Context、宿主ClassLoader、负责和宿主通信的PluginContextImpl类, 最后将Plugin对象存入插件管理进程统一管理.
内置插件的安装是在初始化时就自动安装和加载了
外置插件的安装需要调用Replugin.install()方法来安装插件, 这个过程和内置插件类似, 区别就是内置插件是通过assets目录下的json文件来生成插件对象, 外置插件则是通过获取插件apk的PackageInfo来生成插件对象, 但是并不会处理apk中的dex、so库、资源等, 只有当真正使用这个插件中的类时才会去真正的解析加载这个插件
主程序使用的Gradle插件, 主要职责是在我们的主程序打包的过程中(编译的过程中)动态的修改AndroidManifest.xml的信息, 动态的生成占位各种Activity、provider和service的声明.
其次还会动态生成一个HostBuildConfig的java类, 这个类是根据app下的build.gradle中配置的参数信息(repluginHostConfig)产生的, 这个类的路径在BuildConfig的同级目录的gen包下.
然后会扫描内置插件目录assets/plugins目录, 解析插件文件生成包含文件名、包名、版本、路径的plugins-build.json文件, 这个文件的路径在assets目录.
这个库是由主程序依赖的, 也是RePlugin的核心, 它的主要职责是初始化Replugin的整体框架, 整体框架使用了Binder机制来实现多进程直接的沟通和数据共享, 或者说是插件之间和宿主之间沟通和数据共享, hook住ClassLoader, 加载插件、启动插件、多插件的管理全部都由这个库辅助
这个是插件工程使用的gradle插件, 这个库使用了Transform API和Javassist实现了编译期间动态修改字节码文件, 主要是替换插件工程中的Activity的继承全部替换成Replugin库中定义的XXXActivity, 动态的将插件APK中调用LocalBroadcastManager的地方修改为Replugin中的PluginLocalBroadcastManager调用, 动态修改ContentResolver和ContentProvider的调用修改成Replugin调用, 动态的修改插件工程中所有调用Resource.getIdentifier方法的地方, 将第三个参数修改为插件工程的包名
这个库是由插件工程依赖的, 这个库的主要目的是通过反射的方式来使主程序中接口和功能, 这个库在主程序加载插件apk后悔进行初始化.
2.1 ClassLoader
2.2 RePluginApplication初始化
2.3 PatchClassLoaderUtils.patch
2.4 创建RePluginClassLoader
Android系统默认三个ClassLoader
1. BootClassLoader: 系统启动时创建, 一般不需要用到. 加载系统类
2. PathClassLoader: 应用启动时创建, 只能加载内部dex
3. DexClassLoader: 可以加载外部的dex
Replugin提供了两个ClassLoader
RePluginClassLoader:
宿主APP中的ClassLoader, 加载应用内部的Activity
PluginDexClassLoader:
加载插件的Loader.
public class RePluginClassLoader extends PathClassLoader;
public class PluginDexClassLoader extends DexClassLoader;
调用链:
RepluginApplication.attachBaseContext -> RepluginApplication.App.attachBaseContext
-> PMF.init()
-> PatchClassLoaderUtils.patch()
public static boolean patch(Application application) {
//1.oBase指向ContextImpl: 具体结合LoadedApk.makeApplication()
Context oBase = application.getBaseContext();
// 2.反射获取ComTextImpl.mPackageInfo变量 -> LoadedApk
Object oPackageInfo = ReflectUtils.readField(oBase, "mPackageInfo");
// 3.反射获取LoadedApk.mClassLoader -> ClassLoader = PathClassLoader
ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");
// 外界可自定义ClassLoader的实现,但一定要基于RePluginClassLoader类
// 4.创建RePluginClassLoader
ClassLoader cl = RePlugin.getConfig().getCallbacks().createClassLoader(oClassLoader.getParent(), oClassLoader);
// 将新的ClassLoader写入mPackageInfo.mClassLoader
// 5.将新创建的RePluginClassLoader替换掉LoadedApk中的PathClassLoader
ReflectUtils.writeField(oPackageInfo, "mClassLoader", cl);
// 设置线程上下文中的ClassLoader为RePluginClassLoader
// 防止在个别Java库用到了Thread.currentThread().getContextClassLoader()时,“用了原来的PathClassLoader”,或为空指针
Thread.currentThread().setContextClassLoader(cl);
return true;
}
RePlugin.getConfig().getCallbacks().createClassLoader:
/**
* @param parent 该ClassLoader的父亲,通常为BootClassLoader
* @param original 宿主的原ClassLoader,通常为PathClassLoader
*/
public RePluginClassLoader(ClassLoader parent, ClassLoader orig) {
// 由于PathClassLoader在初始化时会做一些Dir的处理,所以这里必须要传一些内容进来
// 但我们最终不用它,而是拷贝所有的Fields
super("", "", parent);
mOrig = orig;
// 将原来宿主里的关键字段,拷贝到这个对象上,这样骗系统以为用的还是以前的东西(尤其是DexPathList)
// 注意,这里用的是“浅拷贝”
copyFromOriginal(orig);
initMethods(orig);
}
Replugin.install() -> MP.pluginDownloaded()
-> PmHostSvc.pluginDownloaded()
-> PluginManagerServer.Stub.install()
-> PluginManagerServer.installLocked()
/**
* 安装或升级此插件
* 注意:
* 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) {
// 判断文件合法性
File file = new File(path);
if (!file.exists()) {
return null;
} else if (!file.isFile()) {
return null;
}
// 若为p-n开头的插件,则必须是从宿主设置的“插件安装路径”上(默认为files目录)才能安装,其余均不允许
if (path.startsWith("p-n-")) {
String installPath = RePlugin.getConfig().getPnInstallDir().getAbsolutePath();
// 校验文件合法性
if (!path.startsWith(installPath)) {
return null;
}
}
return MP.pluginDownloaded(path);
}
/**
* @param path
* @return
*/
public static final PluginInfo pluginDownloaded(String 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()) {
// 加锁
}
PluginInfo info = PluginProcessMain.getPluginHost().pluginDownloaded(path);
if (info != null) {
RePlugin.getConfig().getEventCallbacks().onInstallPluginSucceed(info);
}
return info;
} catch (Throwable e) {
} finally {
// 去锁
if (lock != null) {
lock.unlock();
}
}
return null;
}
public PluginInfo pluginDownloaded(String path) throws RemoteException {
// 通过路径来判断是采用新方案,还是旧的P-N(即将废弃,有多种)方案
PluginInfo pi;
String fn = new File(path).getName();
if (fn.startsWith("p-n-") || fn.startsWith("v-plugin-") || fn.startsWith("plugin-s-") || fn.startsWith("p-m-")) {
pi = pluginDownloadedForPn(path);
} else {
pi = mManager.getService().install(path);
}
if (pi != null) {
// 通常到这里,表示“安装已成功”,这时不管处于什么状态,都应该通知外界更新插件内存表
syncInstalledPluginInfo2All(pi);
}
return pi;
}
private PluginInfo installLocked(String path) {
// 1.是否开启插件签名校验
final boolean verifySignEnable = RePlugin.getConfig().getVerifySign();
final int flags = verifySignEnable ? PackageManager.GET_META_DATA | PackageManager.GET_SIGNATURES : PackageManager.GET_META_DATA;
/**
* 2.读取APK内容
* PackageInfo主要用于存储获取到的Package的一些信息, 包括:
* packageName: 包名
* versionCode: 版本号
* versionName: 版本名
* firstInstallTime: 首次安装时间
* lastUpdateTime: 最后一次覆盖安装时间
*/
PackageInfo pi = mContext.getPackageManager().getPackageArchiveInfo(path, flags);
if (pi == null) {
// 读取失败, 当前path对应的apk为无效apk
return null;
}
// 3.校验插件签名
if (verifySignEnable) {
if (!verifySignature(pi, path)) {
return null;
}
}
// 4.解析出名字和三元组
PluginInfo instPli = PluginInfo.parseFromPackageInfo(pi, path);
// 5.只是加载并没有安装
instPli.setType(PluginInfo.TYPE_NOT_INSTALL);
// 若要安装的插件版本小于或等于当前版本,则安装失败
// NOTE 绝大多数情况下,应该在调用RePlugin.install方法前,根据云端回传的信息来判断,以防止下载旧插件,浪费流量
// NOTE 这里仅做双保险,或通过特殊渠道安装时会有用
// 注意:这里必须用“非Clone过的”PluginInfo,因为要修改里面的内容
PluginInfo curPli = MP.getPlugin(instPli.getName(), false);
// 6.当前插件存在(可能为旧版本)
if (curPli != null) {
// 版本较老,直接返回
}
// 7.将合法的APK改名后,移动(或复制,见RePluginConfig.isMoveFileWhenInstalling)到新位置
// 注意:不能和p-n的最终释放位置相同,因为管理方式不一样
if (!copyOrMoveApk(path, instPli)) {
return null;
}
// 8.从插件中释放 So 文件
PluginNativeLibsHelper.install(instPli.getPath(), instPli.getNativeLibsDir());
// 9.若已经安装旧版本插件,则尝试更新插件信息,否则直接加入到列表中
if (curPli != null) {
updateOrLater(curPli, instPli);
} else {
mList.add(instPli);
}
// 9.保存插件信息到文件中,下次可直接使用
mList.save(mContext);
return instPli;
}
Replugin.startActivity()
-> Factory.startActivityWithNoInjectCN()
-> PluginCommImpl.startActivity()
-> PluginLibraryInternalProxy.startActivity()
-> PluginCommImpl.loadPluginActivity()//根据目标Activity信息获取坑位信息
public static boolean startActivity(Context context, Intent intent) {
// TODO 先用旧的开启Activity方案,以后再优化
ComponentName cn = intent.getComponent();
if (cn == null) {
// TODO 需要支持Action方案
return false;
}
String plugin = cn.getPackageName();
String cls = cn.getClassName();
return Factory.startActivityWithNoInjectCN(context, intent, plugin, cls, IPluginManager.PROCESS_AUTO);//PROCESS_AUTO: 自动分配进程
}
/**
* 启动一个插件中的activity, 如果插件不存在会触发下载界面
* @param context 应用上下文或Activity上下文
* @param intent
* @param plugin 插件名
* @param activity 待启动的activity类名
* @param process 是否在指定进程中启动
* @param download 下载
* @return 插件机制是否成功, 例如没有插件存在, 没有合适的Activity坑
*/
/**
* Intent from: 要打开的插件的intent
* Intent to: 坑位Intent
*/
public boolean startActivity(Context context, Intent intent, String plugin, String activity, int process, boolean download) {
// 是否启动下载
// 若插件不可用(不存在或版本不匹配),则直接弹出“下载插件”对话框
// 因为已经打开UpdateActivity,故在这里返回True,告诉外界已经打开,无需处理
if (download) {
if (PluginTable.getPluginInfo(plugin) == null) {
// 如果用户在下载即将完成时突然点按“取消”,则有可能出现插件已下载成功,但没有及时加载进来的情况
// 因此我们会判断这种情况,如果是,则重新加载一次即可,反之则提示用户下载
// 原因:“取消”会触发Task.release方法,最终调用mDownloadTask.destroy,导致“下载服务”的Receiver被注销,即使文件下载了也没有回调回来
// NOTE isNeedToDownload方法会调用pluginDownloaded再次尝试加载
if (isNeedToDownload(context, plugin)) {
return ...;
}
}
}
/* 检查是否是动态注册的类 */
// 如果要启动的 Activity 是动态注册的类,则不使用坑位机制,而是直接动态类。
// 原因:宿主的某些动态注册的类不能运行在坑位中(如'桌面'插件的入口Activity)
if (Factory2.isDynamicClass(plugin, activity)) {
intent.putExtra(IPluginManager.KEY_COMPATIBLE, true);
intent.setComponent(new ComponentName(IPC.getPackageName(), activity));
context.startActivity(intent);
return true;
}
// 如果插件状态出现问题,则每次弹此插件的Activity都应提示无法使用,或提示升级(如有新版)
if (PluginStatusController.getStatus(plugin) < PluginStatusController.STATUS_OK) {
return ...;
}
// 若为首次加载插件,且是“大插件”,则应异步加载,同时弹窗提示“加载中”
if (!RePlugin.isPluginDexExtracted(plugin)) {
PluginDesc pd = PluginDesc.get(plugin);
if (pd != null && pd.isLarge()) {
return ...;
}
}
// WARNING:千万不要修改intent内容,尤其不要修改其ComponentName
// 因为一旦分配坑位有误(或压根不是插件Activity),则外界还需要原封不动的startActivity到系统中
// 可防止出现“本来要打开宿主,结果被改成插件”,进而无法打开宿主Activity的问题
// 缓存打开前的Intent对象,里面将包括Action等内容
Intent from = new Intent(intent);
// 帮助填写打开前的Intent的ComponentName信息(如有。没有的情况如直接通过Action打开等)
if (!TextUtils.isEmpty(plugin) && !TextUtils.isEmpty(activity)) {
from.setComponent(new ComponentName(plugin, activity));
}
// 1.获取坑位信息
ComponentName cn = mPluginMgr.mLocal.loadPluginActivity(intent, plugin, activity, process);
if (cn == null) {
return false;
}
// 将Intent指向到“坑位”。这样:
// from:插件原Intent
// to:坑位Intent
intent.setComponent(cn);
context.startActivity(intent);
// 通知外界,已准备好要打开Activity了
// 其中:from为要打开的插件的Intent,to为坑位Intent
return true;
}
public ComponentName loadPluginActivity(Intent intent, String plugin, String activity, int process) {
ActivityInfo ai = null;
String container = null;
PluginBinderInfo info = new PluginBinderInfo(PluginBinderInfo.ACTIVITY_REQUEST);
// 获取 ActivityInfo(可能是其它插件的 Activity,所以这里使用 pair 将 pluginName 也返回)
ai = getActivityInfo(plugin, activity, intent);
if (ai == null) {
return null;
}
// 存储此 Activity 在插件 Manifest 中声明主题到 Intent
intent.putExtra(INTENT_KEY_THEME_ID, ai.theme);
// 根据 activity 的 processName,选择进程 ID 标识
if (ai.processName != null) {
process = PluginClientHelper.getProcessInt(ai.processName);
}
// 容器选择(启动目标进程)
IPluginClient client = MP.startPluginProcess(plugin, process, info);
if (client == null) {
return null;
}
// 远程分配坑位
container = client.allocActivityContainer(plugin, process, ai.name, intent);
// 分配失败
if (TextUtils.isEmpty(container)) {
return null;
}
PmBase.cleanIntentPluginParams(intent);
PluginIntent ii = new PluginIntent(intent);
ii.setPlugin(plugin);
ii.setActivity(ai.name);
ii.setProcess(IPluginManager.PROCESS_AUTO);
ii.setContainer(container);
ii.setCounter(0);
return new ComponentName(IPC.getPackageName(), container);
}
方法调用链
PluginProcessPer.allocActivityContainer()
-> PluginProcessPer.bindActivity()
final String bindActivity(String plugin, int process, String activity, Intent intent) {
/* 获取插件对象 */
Plugin p = mPluginMgr.loadAppPlugin(plugin);
if (p == null) {
// 1.无效的插件名或者加载插件失败
return null;
}
// 2.获取 ActivityInfo
ActivityInfo ai = p.mLoader.mComponents.getActivity(activity);
if (ai == null) {
// 3.Activity不存在
return null;
}
if (ai.processName == null) {
ai.processName = ai.applicationInfo.processName;
}
if (ai.processName == null) {
ai.processName = ai.packageName;
}
// 4.获取 Container
String container;
// 5.自定义进程
if (ai.processName.contains(PluginProcessHost.PROCESS_PLUGIN_SUFFIX2)) {
String processTail = PluginProcessHost.processTail(ai.processName);
container = mACM.alloc2(ai, plugin, activity, process, intent, processTail);
} else {
container = mACM.alloc(ai, plugin, activity, process, intent);
}
if (TextUtils.isEmpty(container)) {
return null;
}
/* 检查 activity 是否存在 */
Class<?> c = null;
c = p.mLoader.mClassLoader.loadClass(activity);
if (c == null) {
return null;
}
return container;
}