zoukankan      html  css  js  c++  java
  • 手把手带你打造一个 Android 热修复框架

    本文来自网易云社区

    作者:王晨彦


    Application 处理

    上面我们已经对所有 class 文件插入了 Hack 的引用,而插入 dex 是在 Application 中,Application 启动前肯定要先加载 Application.class,但这时 dex 还没被插入,因此肯定会引起 ClassNotFoundException ,因此我们不能使 Application 引用 Hack。

    那么修改 class 文件时如何知道哪个是 Application 呢,有人可能会说直接特判不就行了,但是我觉得要作为一个插件的话就要做到兼容,并且尽量减少使用者的手动配置。

    那么如何让插件找到 Application 的名字呢,这时就要用到上面的 processDebugManifest Task 了。

    我们都知道,Application需要在 Manifest 中注册,因此只要找到 Manifest 文件就能得到 Application 的名字了。

    没错,Manifest 文件就在 processDebugManifest 的 outputs.files 中,找到 Manifest 后解析 application 标签即可。

    • 开启混淆会怎样?

    我们正式上线的应用都是会混淆的,我们刚才测试的使用 debug 未混淆模式,如果我们开启混淆的话 Task 还会和上面的完全一样吗?

    我们把 release 的混淆打开,然后执行 assembleRelease,观察 Gradle Console 输出

    :app:preBuild UP-TO-DATE// 省略部分Task:app:processReleaseJavaRes NO-SOURCE
    :app:transformResourcesWithMergeJavaResForRelease
    :app:transformClassesAndResourcesWithProguardForRelease
    :app:transformClassesWithDexForRelease
    :app:mergeReleaseJniLibFolders
    :app:transformNativeLibsWithMergeJniLibsForRelease
    :app:validateSigningRelease
    :app:packageRelease
    :app:assembleRelease

    可以看到相比较未开启混淆多了一个 transformClassesAndResourcesWithProguardForRelease, 那么这个Proguard Task有用吗?

    有用!

    为了保证打包 APK 和 patch 时 class 混淆后的名字不变,我们需要在 Proguard Task 前插入混淆逻辑

    使用 Proguard 的 -applymapping 即可实现。

    因此,我们还要对打包APK后生成的 mapping 文件进行保存。

    插件中代码实现

    static applymapping(TransformTask proguardTask, File mappingFile) {    if (proguardTask) {
            ProGuardTransform transform = (ProGuardTransform) proguardTask.getTransform()        if (mappingFile.exists()) {
                transform.applyTestedMapping(mappingFile)
            } else {
                CFixLogger.i("${mappingFile} does not exist")
            }
        }
    }
    • 补丁签名

    为了安全,上线时我们最好对补丁加上签名验证,保证补丁签名和 APK 签名一致。

    签名使用 JDK 中的 jarsigner

    List<String> command = [JavaEnvUtils.getJdkExecutable('jarsigner'),                        '-verbose',                        '-sigalg', 'MD5withRSA',                        '-digestalg', 'SHA1',                        '-keystore', extension.storeFile.absolutePath,                        '-keypass', extension.keyPassword,                        '-storepass', extension.storePassword,
                            patchFile.absolutePath,
                            extension.keyAlias]Process proc = command.execute()

    校验签名的代码我就不贴了,对应的是源码中的 SignChecker 类。

    检验成果

    上面我们已经把制作补丁,导入补丁的过程大致梳理了一遍,接下来就需要把上面的代码整理一下。

    为了方便使用,我们将其制作为一个 Gradle 插件。如果还不了解如何制作 Gradle 插件的话快点去学习啦

    我已将插件和依赖库上传至 JCenter,在 app 中引入插件。

    // root build.gradlebuildscript {
        repositories {
            jcenter()
        }
    
        dependencies {
            classpath 'com.android.tools.build:gradle:2.3.3'
            classpath 'me.wcy:cfix-gradle:1.1'
        }
    }// app build.gradleapply plugin: 'com.android.application'apply plugin: 'me.wcy.cfix'cfix {
        includePackage = ['me/wcy/cfix/sample'] // 需要插入补丁的包名,一般为应用的包名
        excludeClass = [] // 不需要插入补丁的类
        debugOn = true // debug 模式是否插入补丁
    
        sign = true // 是否添加签名
        storeFile = file("release.jks")
        storePassword = 'android'
        keyAlias = 'cfix'
        keyPassword = 'android'}// 省略部分代码dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile 'com.android.support:appcompat-v7:25.3.1'
        compile 'me.wcy:cfix:1.0'}

    在 Application 中插入 Hack dex 和 patch

    @Overrideprotected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        CFix.init(this);
        CFix.loadPatch(Environment.getExternalStorageDirectory().getPath().concat("/patch.jar"), !BuildConfig.DEBUG);
    }

    首先不对项目做任何修改,直接运行

    熟悉的 Hello World

    检查下 class 文件是否已经引入 Hack 类,编译后的 class 位于 app/build/intermediates/classes

     

    可以看到,Application 没有引入 Hack 类,Activity 已经成功引入 Hack 类。

    然后我们添加一个对话框类,并在Activity中调用该类显示对话框

    public class FixDialog {    public void show(Context context) {        new AlertDialog.Builder(context)
                    .setTitle("Congratulations")
                    .setMessage("Patch Success!")
                    .setPositiveButton("OK", null)
                    .show();
        }
    }// MainActivity@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        FixDialog dialog = new FixDialog();
        dialog.show(this);
    }

    保存生成的 Hash 文件,制作补丁包

    打开终端,执行以下命令

    gradlew clean cfixXiaomiDebugPatch -P cfixDir=D:AndroidAndroidStudioProjectsCFixappcfix

    Xiaomi 表示 productFlavor,Debug 表示 buildType

    将生成的 patch.jar push 到手机 SD 根目录

    adb push D:UserswcyDesktoppatch.jar /sdcard/

    重启应用

    注意,因为我们只是测试,所以把补丁包放在了SD中,因此需要添加读取SD权限,还需要把 targetSdk 改为小于 23 或者手动给予权限。

     

    源码

    该框架可以说是对 Nuwa 的优化升级,几乎支持了目前所有的 Gradle 版本 1.5.0-3.0.1(1.5之前的版本由于太旧未适配)。

    再次对 Nuwa 作者表示感谢,给我们提供了很好的例子。

    该框架在网易七鱼 Android 客服工作台中已经验证通过。

    框架使用方法请参考 README

    声明:该框架未进行兼容性测试,因此不保证兼容所有机型。如果要在商业项目中使用,建议进行兼容性测试。

    总结

    今天我们主要对 QQ 空间的热修复方案进行了可实行性探讨,对整个流程进行梳理,并最终实现了整套方案,验证通过。

    其实我在这期间也踩了不少坑,如 QQ 空间博客中提到的使用 javassist 对 class 进行修改,我使用 javassist 后,一开始在 demo 中可以正常修改 class,但是到了大量代码的线上项目中一直报找不到 v4 包中的类,导致无法修改 class 文件引入 Hack 类。打 log 又发现类已经正常被加载,而且有时能找到有时找不到,每次找不到的类还不一样,WTF。

    最后参考了 Nuwa 的实现,替换为 ASM,问题解决。

    近两年涌现了很多热修复框架,关于热修复的文章也有很多,相信大家也看了不少,但是看的再多,终究不如动手实践来的深刻。


    网易云免费体验馆,0成本体验20+款云产品! 

    更多网易研发、产品、运营经验分享请访问网易云社区


    相关文章:
    【推荐】 机器学习、深度学习、和AI算法可以在网络安全中做什么?
    【推荐】 关于网易云验证码V1.0版本的服务介绍

  • 相关阅读:
    查找数据库表中重复的 Image 类型值
    C#中的引用传递和值传递。
    用JS解决Asp.net Mvc返回JsonResult中DateTime类型数据格式的问题
    根据业务自己设计的.NET工厂模式架构
    封装EF code first用存储过程的分页方法
    2013款MacBook Air装Windows7单系统
    js判断是否在微信浏览器中打开
    EF Code First连接现有数据库
    JS中for循序中延迟加载实现动态效果
    DIV+CSS左右两列自适应高度的方法
  • 原文地址:https://www.cnblogs.com/zyfd/p/9728172.html
Copyright © 2011-2022 走看看