Android游戏开发实战 : 手游sdk热修复

效果展示

写一个有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 类进行真正加载。

外部Class 文件加载

Android 中 提供了 DexClassLoader 用于外部Dex的加载 , 使用也非常简单

 File file = mContext.getDir("plugin-opti", Context.MODE_PRIVATE);
new DexClassLoader(pluginPath, file.getAbsolutePath(), null, mContext.getClassLoader());

DexClassLoader 构造中的几个参数

  • dexPath : 外部apk的路径
  • optimizedDirectory: 外部apk的路径解压优化后的目录
  • librarySearchPath : so 文件的目录
  • parent : 父加载器

外部资源文件加载

传统方式使用 反射 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;
    }

插件apk , 运用ASM 去除公共类

因为公共接口已经存在于宿主中 , 如果不把插件中的公共类去掉 , 向上转型时会报错 , 所以需要用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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值