写一个有bug的init方法用于测试
再写一个bug fix 后的init方法
把有bug和fix_bug的两个包放在 assets中
测试代码
public void loadBugPlugin(View view) {
loadPlugin("bug.apk");
}
public void loadFixBugPlugin(View view) {
loadPlugin("fix_bug.apk");
}
private void loadPlugin(String pluginName) {
Plugin plugin = PluginManager.getInstance(this).loadPlugin(pluginName);
SqWanCore.getInstance(this).setPlugin(plugin);
Toast.makeText(this,"插件加载完成 + 路径+ "+plugin.getPluginPath(),Toast.LENGTH_SHORT).show();
}
public void callPluginInit(View view) {
try {
SqWanCore.getInstance(this).init();
} catch (Exception e) {
Toast.makeText(this,e.getMessage(),Toast.LENGTH_SHORT).show();
}
}
两个按钮 一个按钮点击加载 bug.apk 插件 另一个按钮点击加载fix_bug.apk 插件 , 然后调用插件 init方法
本文 demo 基于37手游安卓团队 sdkHotFix 进行改造
刚学Java的时候,记得有个知识点叫向上转型 , 任何类都可以向上转型为 Object类 , 但是 Object 类在当前类的类加载器中又找不到 , 却又不会报错 , 因为Object 类可以在父加载器中找到 , 原理就是这个 。
private final String SDK_CLASS = "com.sq.mobile.sqsdk.SqWanCoreImpl";
private final String SDK_GET_METHOD = "getInstance";
public void setPlugin(Plugin plugin){
mPlugin =plugin;
try {
//从插件里面获得接口(SQSdkInterface)的具体实现
Class sdkClass = mPlugin.mClassLoader.loadClass(SDK_CLASS);
Method sdkGetMethod = sdkClass.getMethod(SDK_GET_METHOD);
mSdk = (SQSdkInterface) sdkGetMethod.invoke(null);
mSdk.setPluginInterface(this);
} catch (Exception e) {
//ClassNotFoundException, NoSuchMethodException,
//InvocationTargetException, IllegalAccessException
e.printStackTrace();
}
}
SQSdkInterface 作为公共的接口 , 宿主 和 插件中都存在
SqWanCoreImpl 存在于插件中 , 继承自SQSdkInterface
首先使用DexClassLoader 加载插件中类 , 然后向上转型为 SQSdkInterface , 通过SQSdkInterface 调用插件中的方法
在加载字节码的时候,会询问当前 ClassLoader 是否已经加载过,如果加载过则直接返回,不再重复加载,如果没有的话,会查询 parent 是否加载过,如果加载过,就直接返回 parent 加载的字节码文件。如果整个继承线路上的 ClassLoader 都没有加载,执行类才会由当前 ClassLoader 类进行真正加载。
Android 中 提供了 DexClassLoader 用于外部Dex的加载 , 使用也非常简单
File file = mContext.getDir("plugin-opti", Context.MODE_PRIVATE);
new DexClassLoader(pluginPath, file.getAbsolutePath(), null, mContext.getClassLoader());
DexClassLoader 构造中的几个参数
传统方式使用 反射 addAssetPath 给AssetManager 添加插件资源目录 , 考虑后续适配问题 , 使用Shadow 方式 比较好 , 通过 context.getPackageManager().getResourcesForApplication 构建一个 Resources 给插件使用
private Resources buildPluginResources() {
try {
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_SIGNATURES);
String hostPublicSourceDir = packageInfo.applicationInfo.publicSourceDir;
String hostSourceDir = packageInfo.applicationInfo.sourceDir;
String pkgName = packageInfo.packageName;
packageInfo.applicationInfo.publicSourceDir = mPluginPath;
packageInfo.applicationInfo.sourceDir = mPluginPath;
PackageInfo pluginInfo = mContext.getPackageManager().getPackageArchiveInfo(mPluginPath, PackageManager.GET_ACTIVITIES);
mPluginPkgName = pluginInfo.packageName;
Resources pluginResources = mContext.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);
packageInfo.applicationInfo.publicSourceDir = hostPublicSourceDir;
packageInfo.applicationInfo.sourceDir = hostSourceDir;
packageInfo.packageName = pkgName;
return pluginResources;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
因为公共接口已经存在于宿主中 , 如果不把插件中的公共类去掉 , 向上转型时会报错 , 所以需要用ASM 在编译期间把公共的接口从宿主中去掉 插件中使用的功能 以模块的形式依赖 , 便于解耦
使用 asm 把 commonLib 的类在打包时候去掉关键代码如下
val needDeleteClasses = arrayListOf(
"com/sq/mobile/commonpluhostandsqsdk/SQSdkInterface.class",
"com/sq/mobile/commonpluhostandsqsdk/BasePluginInterface.class"
);
fun modifyJarFile(jarFile: File, tempDir: File?, transform: TransformInvocationHelper): File {
/** 设置输出到的jar */
val hexName = DigestUtils.md5Hex(jarFile.absolutePath).substring(0, 8)
val optJar = File(tempDir, hexName + jarFile.name)
val jarOutputStream = JarOutputStream(FileOutputStream(optJar))
jarOutputStream.use {
val file = JarFile(jarFile)
val enumeration = file.entries()
enumeration.iterator().forEach { jarEntry ->
val inputStream = file.getInputStream(jarEntry)
val entryName = jarEntry.name
if (entryName.contains("module-info.class") && !entryName.contains("META-INF")) {
printThis("jar file module-info:$entryName jarFileName:${jarFile.path}")
} else {
printThis("entryName =$entryName")
if (!needDeleteClasses.contains(entryName)){
val zipEntry = ZipEntry(entryName)
jarOutputStream.putNextEntry(zipEntry)
var modifiedClassBytes: ByteArray? = null
val sourceClassBytes = inputStream.readBytes()
if (entryName.endsWith(".class")) {
try {
modifiedClassBytes = transform.process(entryName, sourceClassBytes)
} catch (ignored: Exception) {
}
}
/**
* 读取原jar
*/
if (modifiedClassBytes == null) {
jarOutputStream.write(sourceClassBytes)
} else {
jarOutputStream.write(modifiedClassBytes)
}
jarOutputStream.closeEntry()
}
}
}
}
return optJar
}
asm 输入的文件 , 分为目录 和 jar 包 , commonLib 的文件 就在jar包中 , 上面的代码就是将 公共的接口从jar 包中去掉。
作者:jiangpan
链接:https://juejin.cn/post/7123469377080393759
来源:稀土掘金
留言与评论(共有 0 条评论) “” |