Android中实现热补丁动态修复

原创 2017年07月17日 16:52:33

文章背景

在做互联网app项目的时候,当我们发布迭代了一个新版本,把apk发布到各个Android应用市场上时,由于程序猿或是程序媛在编码上的疏忽,突然出现了一个紧急Bug时,通常的做法是重新打包,重新发布到各个应用市场,这不仅给公司相关部门增加大量工作量外,好比古时候皇帝下放一道紧急命令时,从州到县到镇到村,整条线都提着脑袋忙得不可交,搞的人心惶惶,而且更严重的是最终给用户带来的是重新下载覆盖安装,在一定程度上会流失用户,严重影响了公司的用户流量。在这种场景我们应该采用热补丁动态修复技术来解决以上这些问题。可以选择现成的第三方热修复SDK,我在这里不选择的原因,主要出于两点:1、使用第三方SDK有可能增大我们的项目包,而且总感觉受制于人;2、追逐技术进阶

文章目标

Android类加载机制介绍
javassist动态修改字节码
实现热补丁动态修复

Android类加载机制

1.ClassLoader体系结构

classloader

2、如何加载一个类

我们先来看一下BaseDexClassLoader源码中比较重要的code

cl11

根据截图可以看到里面有一个findClass方法,没错它就是根据类名来查找指定的某一个类。然后在该方法中调用了 DexPathList 实例的pathList.findClass(name, suppressedExceptions)的方法,我们进到这个方法看看

cl12

可以看出最终在此处找到了某一个类

 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);

到这里我们可以直观的看出该过程是基于android dex分包方案的。其实最终我们打包apk时可能有一个或是多个dex文件,默认是一个叫classes.dex的文件。不管是一个还是多个,都会一一对应一个Element,按顺序排成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

按照这个原理,我们可以把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,当遍历findClass的时候,我们修复的类就会被查找到,从而替代有bug的类即可,那么下面来进行这一个过程的操作吧。

patch.dex补丁制作

新建一个Hotfix的工程,然后新建一个BugClass类

/*
 * Copyright (C) 2015 Baidu, Inc. All Rights Reserved.
 */
package ydc.hotfix;


/**
 * Created by sunpengfei on 15/11/3.
 */
public class BugClass {

    public String bug() {
        return "fix bug class";
    }
}

在新建一个LoadBugClass类

/*
 * Copyright (C) 2015 Baidu, Inc. All Rights Reserved.
 */
package ydc.hotfix;

/**
 * Created by sunpengfei on 15/11/4.
 */
public class LoadBugClass {
    public String getBugString() {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}

注意LoadBugClass应用了BugClass类。

然后在界面层是这样调用的:
13

ok,假设我们把该apk发布出去了,那么用户看到效果应该是“ 测试调用方法:fix bug class”。这个时候公司领导认为这样的提示对于用户是致命的。那么我们要把BugClass 类中的bug()方法中字符串替换一下,仅仅是修复一句话而已,实在没有必要走打包发布下放市场等复杂的流程。

public String bug() {
        return "fix bug class";
    }

ok,把这个有问题的地方修正为:

  public String bug() {
        return "杨德成正在修复提示语fix bug class";
    }

ok,我们把BugClass类使用dex工具单独打包成path_dex.jar补丁包

Step

1、配置dex环境变量,最好是对应版本。

cl14

2、验证dex

cl15

3、先把BugClass.class文件做成成jar,注意路径,一定要定位到该位置执行以下命令:

jar cvf path.jar ydc/hotfix/BugClass.class

cl16

4、再把path.jar做成补丁包path_dex.jar,只有通过dex工具打包而成的文件才能被Android虚拟机(dexopt)执行。
依然在该路径下执行以下命令:

dx --dex --output=path_dex.jar path.jar

cl17

5、我们把path_dex文件拷贝到assets目录下

cl18

ok,这个时候我们可以开始来打补丁

Step

1、将我们的补丁包path_dex插入到上面提到的装有dex的有序数组dexElements的最前面

首先我们看一下hotfix的源码:

cl19

根据截图所示,做了两个动作

a、创建一个私有目录,并把补丁包文件写入到该目录下
a1、 创建私有目录

 File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar");

a2、文件读写方式把补丁包文件写入到刚创建的私有目录下


public class Utils {
    private static final int BUF_SIZE = 2048;

    public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {
        BufferedInputStream bis = null;
        OutputStream dexWriter = null;

        try {
            bis = new BufferedInputStream(context.getAssets().open(dex_file));
            dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
            byte[] buf = new byte[BUF_SIZE];
            int len;
            while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
                dexWriter.write(buf, 0, len);
            }
            dexWriter.close();
            bis.close();
            return true;
        } catch (IOException e) {
            if (dexWriter != null) {
                try {
                    dexWriter.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
            return false;
        }
    }
}

b、path_dex插入到上面提到的装有dex的有序数组dexElements的最前面

patch方法中的代码如下:

 public static void patch(Context context, String patchDexFile, String patchClassName) {
        if (patchDexFile != null && new File(patchDexFile).exists()) {
            try {
                if (hasLexClassLoader()) {
                    injectInAliyunOs(context, patchDexFile, patchClassName);
                } else if (hasDexClassLoader()) {
                    injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
                } else {

                    injectBelowApiLevel14(context, patchDexFile, patchClassName);

                }
            } catch (Throwable th) {
            }
        }
    }

根据代码所示,这根据传入的文件类型类类加载器ClassLoader的类型做了下判断,根据上文提到过的ClassLoader 体系原理,我们的补丁包应该走的是hasDexClassLoader()分支,该方法代码如下:

private static boolean hasDexClassLoader() {
        try {
            Class.forName("dalvik.system.BaseDexClassLoader");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

系统中肯定会存在”dalvik.system.BaseDexClassLoader”类,那么接下来应该进入injectAboveEqualApiLevel14(context, patchDexFile, patchClassName)方法,代码如下:

 private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
        throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
            getDexElements(getPathList(
                new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
        Object a2 = getPathList(pathClassLoader);
        setField(a2, a2.getClass(), "dexElements", a);
        pathClassLoader.loadClass(str2);
    }

根据Android系统源码解读源以上代码

PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

根据context拿到PathClassLoader,还记得这个类是用来干嘛的吗,上面已经提到过,再次提醒一下它是用来加载安装到Android系统中的apk文件。既然这样我们可以用它来得到没有打补丁之前的dexElements有序数组对象

Step

a、getPathList(pathClassLoader)方法解读:

private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
        IllegalAccessException {
        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

根据以上代码片段,可以看出这里根据引用类名称”BaseDexClassLoader”查找有个叫”pathList”属性名的被引用类型。

 private static Object getField(Object obj, Class cls, String str)
        throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        return declaredField.get(obj);
    }

上面这个片段通过反射找到对应的被引用类”DexPathList”,上个”BaseDexClassLoader”系统源码:

cl20

b、getDexElements(getPathList(pathClassLoader))方法解读:

private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), "dexElements");
    }

上面的这个代码片段根据a步骤得到的DexPathList对象获取到了没有打补丁之前的dexElements有序数组对象

private static Object getField(Object obj, Class cls, String str)
        throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        return declaredField.get(obj);
    }

根据代码可知依然使用反射原理获取DexPathList对象中的有序数组dexElements。

DexPathList类系统源码如下:

cl21

将我们的补丁包path_dex.jar转化为dexElements对象

Step

a、根据我们在上面所创建的私有目录及私有文件,创建一个DexClassLoader,还记得这个来是用来干嘛的吗,上面已经提到到,再次提醒一下,用来加载从.jar文件内部加载classes.dex文件,没错我们要用它来加载我们的补丁包文件。

 new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))

根据该类的系统源码看出其实该类的构造函数并没有做具体的事情

cl22

真正做之情的是它的直接父类BaseDexClassLoader的构造函数,如图所示

cl23

 this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);

看到没,根据传入参数初始化了我们补丁包对应的 DexPathList对象,注意这一步仅仅是初始化哦

b、getPathList(
new DexClassLoader(str, context.getDir(“dex”, 0).getAbsolutePath(), str, context.getClassLoader()))方法解读:

private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
        IllegalAccessException {
        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }
private static Object getField(Object obj, Class cls, String str)
        throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        return declaredField.get(obj);
    }

上面这两段代码根据引用名”dalvik.system.BaseDexClassLoader”和被引用类属性名”pathList”得到DexPathList对象

c、然后调用getDexElements方法

 private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), "dexElements");
    }
private static Object getField(Object obj, Class cls, String str)
        throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        return declaredField.get(obj);
    }

上面的这两端片段根据 DexPathList类及属性名dexElements获取到我们补丁包对应的有序数组dexElements

上面已经得到了两个有序数组dexElements,一个存放的的是没有打补丁之前的dex有序数组dexElements,另外一个是我们的补丁包对应的dex有序数组dexElements,那么是不是到了该合并两个数组的时候了呢,没错

 Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
            getDexElements(getPathList(
                new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));

到这里终于知道这整句代码到底干了什么事情了,Object a 就是我们合并后的有序dex数组dexElements

合并过程如下:

private static Object combineArray(Object obj, Object obj2) {
        Class componentType = obj2.getClass().getComponentType();
        int length = Array.getLength(obj2);
        int length2 = Array.getLength(obj) + length;
        Object newInstance = Array.newInstance(componentType, length2);
        for (int i = 0; i < length2; i++) {
            if (i < length) {
                Array.set(newInstance, i, Array.get(obj2, i));
            } else {
                Array.set(newInstance, i, Array.get(obj, i - length));
            }
        }
        return newInstance;
    }

其实就是把补丁包对应的dex插入到原来有序数组dexElements的最前面了。

d、得到最新的”PathList”对象

  Object a2 = getPathList(pathClassLoader);

e、重新设置DexPathList 的有序数组对象dexElements值

setField(a2, a2.getClass(), “dexElements”, a);

private static void setField(Object obj, Class cls, String str, Object obj2)
        throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        declaredField.set(obj, obj2);
    }

依然是使用反射机制设置新值。

f、加载我们有bug的类

 pathClassLoader.loadClass(str2);

str参数是通过以下代码传入,即(ydc.hotfix.BugClass)

 HotFix.patch(this, dexPath.getAbsolutePath(), "ydc.hotfix.BugClass");

这时候loadClass到的就是我们补丁包中的BugClass类了,这是因为我们把补丁包对应的dex文件插入到dexElements最前面。所以找到就BugClass直接返回了,代码如下:

 public Class findClass(String name, List<Throwable> suppressed) {
317        for (Element element : dexElements) {
318            DexFile dex = element.dexFile;
319
320            if (dex != null) {
321                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
322                if (clazz != null) {
323                    return clazz;
324                }
325            }
326        }
327        if (dexElementsSuppressedExceptions != null) {
328            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
329        }
330        return null;
331    }

按照我们之前的推论,到这里应该就完成了补丁动态修复了,那么真的是这样的吗,我们不防运行下项目看看。

很不幸,运行时报错:

cl24

这是由于LoadBugClass引用了BugClass,但是发现这这两个类所在的dex不在一起,其中:
1. LoadBugClass在classes.dex中
2. BugClass在path_dex.jar中
结果发生了错误。

究其原因是 pathClassLoader.loadClass(str2)的时候,会去校验LoadBugClass所在的dex和BugClass所在的dex是否是同一个,不是则会报错。那么校验的前提是有一个叫CLASS_ISPREVERIFIED的类标志,如果引用者被打上这个标识,就会去校验,就会导致报错,那么我们可以想象如果引用者LoadBugClass 没被打上这个标识,是否就会运行通过了呢,没错,就是这个原理。

阻止LoadBugClass打上CLASS_ISPREVERIFIED标志

我们应该知道LoadBugClass引用了BugClass,类加载器是先加载引用者,所以我在LoadBugClass的构造方法中来做这件事情,其实我们要做的就是动态的在构造方法中,引用一个别的类,然后把这个被引用类打包成一个单独的dex文件。这样就可以防止了LoadBugClass类被打上CLASS_ISPREVERIFIED的标志了,那我们现在来开始做这件事情。

Step

1、动态被注入类的制作

a、新建一个hackdex的Module,我这里来自HotFix的源码,你也可以自己新建

cl25

b、在该Module之下,新建一个AntilazyLoad空类。

package dodola.hackdex;

/**
 * Created by sunpengfei on 15/11/3.
 */
public class AntilazyLoad {
}

c、打包成单独的dex文件,打包步骤完全等同于补丁包的制作,所以我这里就不在走这个过程了,然后把它放置在assets下

cl26

d、依然要把这个dex文件插入到dexElements有序数组的中,插入原理和补丁包插入原理完全一致,而且这个dex文件需要在程序的入口进行插入,保证它是在有序数组的最前面,因为我们要把该dex文件中的AntilazyLoad要动态注入到其它包里面的某一个类的构造方法中。切记,dexElements里面可以塞入无数个dex文件。


/**
 * Created by sunpengfei on 15/11/4.
 */
public class HotfixApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

ok,下面就是如何注入的问题了,这个时候应该到了我们的AOP三剑客之一”javassist”闪亮登场了。

javassist实现动态代码注入

javassist这货是个好东西啊,它可以以无侵入的方式重构你的原代码。我之前编写过另外一个三剑客之一的文章,原理基本一样。参考地址:http://blog.csdn.net/xinanheishao/article/details/74082605

Step

a、创建buildSrc模块,这个项目是使用Groovy开发的,据说这货具备Java, Javascript, Phython, Ruby等等语言的优点,而且Groovy依赖于Java的,和Java无缝挂接的,你可以到这里下载SDK:http://groovy-lang.org/download.html;然后,配置path环境变量,Groovy的安装挺简单的,基本上和JDK的安装差不多, 当然,这是Groovy自带的最基本的开发工具,你可以查看它如何支持as的,如果是eclipse的话选择菜单项“Help->Install New Software”之后重启eclipse工具即可利用eclipse开发Groovy应用程序了,但是工程名一定要叫”buildSrc”,这里我就直接使用了HotFix,你也可以自己构建,若你觉得闲麻烦,也可以下载我的demo里面获取。

cl27

b、导入javassist


apply plugin: 'groovy'

repositories {
    mavenCentral()
}

dependencies {
    compile gradleApi()
    compile 'org.codehaus.groovy:groovy-all:2.3.6'
    compile 'org.javassist:javassist:3.20.0-GA'
}

c、PatchClass 代码截图如下

cl28

其实很简单的,这几句的意思就是通过反射相关类,然后在相关类的构造方法中插入一句输出语句。

 CtClass c = classes.getCtClass("ydc.hotfix.BugClass")
        if (c.isFrozen()) {
            c.defrost()
        }
        println("====添加构造方法====")
        def constructor = c.getConstructors()[0];
        constructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
        //constructor.insertBefore("System.out.println(888);")
        c.writeFile(buildDir)

执行完这段代码之后,也无形中应用了AntilazyLoad这个类。

d、这个工程不需要引用到主app(Module)中,只需要在 app->build.gradle中配置一个任务:

cl29

在配置一下,侵入时期

cl30

ok,总算把整个过程写完了,准备开始运行了,不管你激不激动,反正本人是挺激动的了。

在运行之前,先看一下我们的引用者类

cl31

没错,可以确认这是我们的源代码,化成灰我也可以认出它来。

在看一下运行之后的引用者类

cl32

没错,就是这个效果,我们的源码被javassist 赤裸裸的侵犯了,是不是瞬间觉的自己的“东西”很不安全,这就是AOP编程的强大之处啊。

项目讲解到这里,我想估计没有几个人能有耐心的看到这里来了,因为觉得文章实在太长,需要有多大耐心才能扛到这里,连我自己也怀疑自己如何写出来的,不过我认为,这么强大而且实用的技术点,不是能够三五两语就能说清的,我们要有足够的耐心来探索我们所不知的,有耐心,我们就有希望,有希望就不会失望!

ok,我们见证一下奇迹。

cl33

看到这效果,我手已累,键盘已坏。。。。

Demo下载地址:
http://download.csdn.net/download/xinanheishao/9902530

演示环境:
demo导入不能正常运行,建议先调整环境,跑起来,再进阶。

 classpath 'com.android.tools.build:gradle:1.3.0'
#Thu Jul 13 16:40:06 CST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip

如果默认的jdk环境找不到,手动指向一下

cl34

如果你已经准备好足够信心的话,可以按照文章,自己尝试一方

最后感谢腾讯空间给出的解决方法思路和HotFix开源作者。

如果对你有所帮助的话,赏我1元奶粉钱吧,多谢!

微信:

001

支付宝:

002

相关文章推荐

安装 Groovy和简单使用

如果您以前从未使用过 Groovy,则首先需要安装 Groovy。安装步骤非常简单,这些步骤与安装 Ant 和 Tomcat 等常见 Java 应用程序甚至安装 Java 平台本身的步骤相同:下载 ...

Android中的AOP编程之AspectJ实战实现数据埋点

文章背景 最近在给某某银行做项目的时,涉及到了数据埋点,性能监控等问题,那我们起先想到的有两种方案,方案之一就是借助第三方,比如友盟、Bugly等,由于项目是部署在银行的网络框架之内的,所以该方案不可...

Android 热补丁动态修复框架小结

转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/49883661; 本文出自:【张鸿洋的博客】 一、概述最新githu...

Android热补丁动态修复技术

Android热补丁动态修复技术(一):从Dex分包原理到热补丁 一、参考 博文:安卓App热补丁动态修复技术介绍——by QQ空间终端开发团队  博文:Android dex分...

Android 热补丁动态修复框架小结

转载请标明出处:  http://blog.csdn.net/lmj623565791/article/details/49883661;  本文出自:【张鸿洋的博客】 一、概述 ...

Android 热补丁动态修复总结 eclipse版

参考文章 1. http://blog.csdn.net/lmj623565791/article/details/49883661  鸿翔博客 2. https://mp.weixin....

Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!

一、前言上一篇博客中,我们通过介绍dex分包原理引出了Android的热补丁技术,而现在我们将解决两个问题。 1. 怎么将修复后的Bug类打包成dex 2. 怎么将外部的dex插入到ClassLo...

Android 热补丁动态修复框架总结

一、概述 最新github上开源了很多热补丁动态修复框架,大致有: https://github.com/dodola/HotFixhttps://github.com/jasonross...

Android AndFix热补丁动态修复框架使用教程

简介已经上线的项目发现BUG,紧急修复BUG发布新版本?No,也许你需要AndFix。AndFix 是阿里巴巴开源的 Android 应用热修复工具,帮助 Anroid 开发者修复应用的线上问题。An...

Android热补丁动态修复实践

原文 : http://blog.csdn.net/wwj_748/article/details/50835720
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Android中实现热补丁动态修复
举报原因:
原因补充:

(最多只允许输入30个字)