Android之热修复实战

3,121 阅读4分钟

1.热修复原理

参考文档:

安卓App热补丁动态修复技术介绍

Android N混合编译与对热补丁影响解析

什么是热修复

定义:在我们应用上线后出现bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户无感知下修复掉bug。

怎么进行热修复

服务端:补丁包管理

用户端:执行热修复

开发端:生成补丁包

热修复需要解决的问题

开发端

  • 补丁包是什么?
  • 如何生成补丁包?
  • 开启混淆后呢?
  • 对比改动自动生成补丁包(gradle)?

用户端

  • 什么时候执行热修复?
  • 怎么执行热修复(使用补丁包)?
  • Android版本兼容问题?

热修复解决方案

热补丁方案有很多,其中比较出名的有腾讯Tinker、阿里的AndFix、美团的Robust以及QZone的超级补丁方案。

AndFix

在native动态替换java层的方法,通过native层hook java层的代码。

Robust

对每个函数都在编译打包阶段自动的插入了一段代码。类似于代理,将方法执行的代码重定向到其他方法中。

Tinker

Tinker通过计算对比指定的Base Apk中的dex与修改后的Apk中的dex的区别,补丁包中的内容即为两者差分的描述。

运行时将Base Apk中的dex与补丁包进行合成,重启后加载全新的合成后的dex文件。

Qzone

2.ClassLoader

双亲委托机制

某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。

1、避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

2、安全性考虑,防止核心API库被随意篡改。

在线源码阅读:www.androidos.net.cn/或http://and…

类查找流程

类加载实现热修复

类是怎么被加载的? 怎么使用补丁包中的类? 已经被加载过的类还能够替换修复吗? 怎样保证补丁包中正确class 的dex先加载?

加载修复后的类

EnjoyFix.installPatch(this, new File("/sdcard/patch.jar"));

EnjoyFix.class

public class EnjoyFix {

    private static final String TAG = "EnjoyFix";

    private static File initHack(Context context) {
        File hackFile = new File(context.getExternalFilesDir(""), "hack.dex");
        FileOutputStream fos = null;
        InputStream is = null;
        try {
            fos = new FileOutputStream(hackFile);
            is = context.getAssets().open("hack.dex");
            int len;
            byte[] buffer = new byte[2048];
            while ((len = is.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
        return hackFile;
    }

    /**
     * 1、获取程序的PathClassLoader对象
     * 2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
     * 3、反射获取pathList的dexElements对象 (oldElement)
     * 4、把补丁包变成Element数组:patchElement(反射执行makePathElements)
     * 5、合并patchElement+oldElement = newElement (Array.newInstance)
     * 6、反射把oldElement赋值成newElement
     *
     * @param application
     * @param patch
     */
    public static void installPatch(Application application, File patch) {
        File hackDex = initHack(application);
        List<File> patchs = new ArrayList<>();
        patchs.add(hackDex);
        if (patch.exists()) {
            patchs.add(patch);
        }

        //1、获取程序的PathClassLoader对象
        ClassLoader classLoader = application.getClassLoader();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            try {
                ClassLoaderInjector.inject(application, classLoader, patchs);
            } catch (Throwable throwable) {
            }
            return;
        }
        //2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
        try {
            Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
            Object pathList = pathListField.get(classLoader);
            //3、反射获取pathList的dexElements对象 (oldElement)
            Field dexElementsField = ShareReflectUtil.findField(pathList, "dexElements");
            Object[] oldElements = (Object[]) dexElementsField.get(pathList);
            //4、把补丁包变成Element数组:patchElement(反射执行makePathElements)
            Object[] patchElements = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                Method makePathElements = ShareReflectUtil.findMethod(pathList, "makePathElements",
                        List.class, File.class,
                        List.class);
                ArrayList<IOException> ioExceptions = new ArrayList<>();
                patchElements = (Object[])
                        makePathElements.invoke(pathList, patchs, application.getCacheDir(), ioExceptions);

            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                Method makePathElements = ShareReflectUtil.findMethod(pathList, "makeDexElements",
                        ArrayList.class, File.class, ArrayList.class);
                ArrayList<IOException> ioExceptions = new ArrayList<>();
                patchElements = (Object[])
                        makePathElements.invoke(pathList, patchs, application.getCacheDir(), ioExceptions);
            }


            //5、合并patchElement+oldElement = newElement (Array.newInstance)
            //创建一个新数组,大小 oldElements+patchElements
//                int[].class.getComponentType() ==int.class
            Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
                    oldElements.length + patchElements.length);

            System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
            System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);
            //6、反射把oldElement赋值成newElement
            dexElementsField.set(pathList, newElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.插桩式热修复运行期修复落地

热修复流程

1.获取到当前应用的PathClassloader; 2.反射获取到DexPathList属性对象pathList; 3.反射修改pathList的dexElements

  • 把补丁包patch.dex转化为Element[] (patch)
  • 获得pathList的dexElements属性(old)
  • patch+dexElements合并,并反射赋值给pathList的dexElements

Element数组

补丁包其实就是一个dex或者包含dex的jar包。怎么把dex变成Element?

Android N混合编译

ART 是在 Android KitKat(Android 4.0)引入并在 Lollipop(Android 5.0)中设为默认运行环境,可以看作Dalvik2.0。ART模式在Android N(7.0)之前安装APK时会采用AOT(Ahead of time:提前编译、静态编译)预编译为机器码。而在Android N使用混合模式的运行时。应用在安装时不做编译,而是运行时解释字节码,同时在JIT编译了一 些代码后将这些代码信息记录至Profile文件,等到设备空闲的时候使用AOT(All-Of-the-Time compilation:全 时段编译)编译生成称为app_image的base.art(类对象映像)文件,这个art文件会在apk启动时自动加载(相当 于缓存)。根据类加载原理,类被加载了无法被替换,即无法修复。

解决方案

运行时替换PathClassLoader方案 App image中的class是插入到PathClassloader中的ClassTable中。假设我们完全废弃掉PathClassloader,而 采用一个新建Classloader来加载后续的所有类,即可达到将cache无用化的效果。

CLASS_ISPREVERIFIED异常

如果MainActivity类中只引用了:Utils类。当打包dex时, MainActivity与Utils都在classes.dex中,则加载 时MainActivity类被标记为 CLASS_ISPREVERIFIED。

如果使用补丁包中的Utils类取代出现bug的Utils,则会导致MainActivity与其引用的Utils不在同一个Dex, 但MainActivity已经被打上标记,此时出现冲突。导致校验失败!

解决方案

阻止标记:防止类被打上CLASS_ISPREVERIFIED标志。

AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的 AntilazyLoad类,防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。

字节码插桩:在Java字节码Class中某些位置插入或修改一些代码。

4.字节码插桩

在Java字节码中某些位置插入或修改一些代码。

Android打包流程

  • AAPT :打包资源文件,并生成R.java和resources.arsc文件。
  • AIDI : 读取.aidl文件生成对应Java文件
  • JAVAC : 将java文件编译成.class文件;
  • DX : 多个.class文件转换打包为.dex文件

ASM

操作Java 字节码的框架,按照Class文件的格式,解析、修改、生成Class,可以动态生成类或者增强现有类的 功能。 正如GSON操作json的框架

5.Gradle插件开发

插件开发

apply plugiin: 'com.android.application'
apply plugiin: 'com.android.library'
方式说明
Build script脚 本把插件写在 build.gradle 文件中,一般用于简单的逻辑,只在该 build.gradle 文件可见。
buildSrc目录将插件源代码放在 buildSrc/src/main/groovy/ 中,只对该项目中可见。
独立项目独立项目 一个独立的 Java 项目/模块,可以将文件包发布到仓库(Jcenter), 使其他项目方便引入。

插件实现

正如我们开发Android项目,创建Activity需要继承android.app.Activity或其子类,插件需 要实现org.gradle.api.Plugin<Project>接口。

右值为实现了插件结构的类全限定名

插件扩展

引入某些插件可能会需要进行插件配置,如:

这些信息由使用者配置,目的是为了能够在插件执行时获得用户配置信息,进行不同的逻辑处理。

 project.getExtensions().create("xx", JavaBean.class);

依赖插件的插件

一般情况下,我们自定义插件主要为了Android项目使用,在插件执行过程中,希望动态获得 android插件中的信息,如:APK输出目录、包名、变体(debug/release)等信息。 我们的插件需要用过Android插件编写的API获取这些参数,因此我们的插件需要依赖Android 插件。 以独立模块开发插件为例,独立模块的build.gradle中加入:

Task

Task,即任务。Android工程引入的Android插件中,创建了多个自定义任务,如:编译Java文件、生成 dex文件、打包等等,每个任务执行一项工作。

6.自动生成补丁包

  1. 编译时生成一份缓存文件,里面记录了所有class文件的md5,以及mapping混淆文件。
  2. 在后续的版本中使用-applymapping选项,应用mapping文件实现每次混淆类名相同,然后计算编译 完成后的class文件的md5和缓存进行比较,把不相同的class文件打包成补丁包。

代码待补充...