Android 中插件化学习—教你实现热补丁动态修复

文章背景

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

文章目标

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

    1.ClassLoader体系结构

    classloader
    classloader

二、如何加载一个类

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

cl11
cl11

  • 根据截图能够看到里面有一个findClass方法
  • 它就是根据类名来查找指定的某一个类
  • 而后在该方法中调用了 DexPathList 实例的pathList.findClass(name, suppressedExceptions)的方法

cl12
cl12

能够看出最终在此处找到了某一个类android

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类数组

package ydc.hotfix;
public class BugClass {

    public String bug() {
        return "fix bug class";
    }
}复制代码

在新建一个LoadBugClass类安全

public class LoadBugClass {
    public String getBugString() {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}复制代码

注意 LoadBugClass应用了BugClass类。bash

而后在界面层是这样调用的:微信

13
13

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

public String bug() {
        return "fix bug class";
    }复制代码

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

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

经过dex工具单独打包成path_dex.jar补丁包

  • 一、配置dex环境变量,最好是对应版本。
    cl14
    cl14
  • 二、验证dex

cl15
cl15

三、先把BugClass.class文件作成成jar,注意路径,必定要定位到该位置执行如下命令:

jar cvf path.jar ydc/hotfix/BugClass.class复制代码

cl16
cl16

  • 四、作成补丁包path_dex.jar
    再把path.jar作成补丁包path_dex.jar,只有经过dex工具打包而成的文件才能被Android虚拟机(dexopt)执行。

依然在该路径下执行如下命令:

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

cl17
cl17

  • 五、拷贝path_dex

    咱们把path_dex文件拷贝到assets目录下

    cl18
    cl18

开始来打补丁

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

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

cl19
cl19

根据截图所示,作了两个动做复制代码
  • a、建立一个私有目录,并把补丁包文件写入到该目录下

    • a一、 建立私有目录
      File dexPath = new File(getDir(“dex”, Context.MODE_PRIVATE), “path_dex.jar”);复制代码
    • a二、文件读写方式把补丁包文件写入到刚建立的私有目录下
    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();复制代码

获得没有打补丁以前的dexElements有序数组对象

  • 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
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
    cl21

    补丁包path_dex.jar转化为dexElements对象

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

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

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

cl22
cl22

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

cl23
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) {
       for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
              Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
              if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }复制代码

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

很不幸,运行时报错:

cl24
cl24

-这是因为LoadBugClass引用了BugClass,可是发现这这两个类所在的dex不在一块儿,其中:

  • LoadBugClass在classes.dex中
  • BugClass在path_dex.jar中结果发生了错误。

    究其缘由是 pathClassLoader.loadClass(str2)的时候,会去校验LoadBugClass所在的dex和BugClass所在的dex是不是同一个,不是则会报错。那么校验的前提是有一个叫CLASS_ISPREVERIFIED的类标志,若是引用者被打上这个标识,就会去校验,就会致使报错,那么咱们能够想象若是引用者LoadBugClass 没被打上这个标识,是否就会运行经过了呢,没错,就是这个原理。

阻止LoadBugClass打上CLASS_ISPREVERIFIED标志

  • 咱们应该知道LoadBugClass引用了BugClass,类加载器是先加载引用者
  • 因此我在LoadBugClass的构造方法中来作这件事情,其实咱们要作的就是动态的在构造方法中,引用一个别的类
  • 而后把这个被引用类打包成一个单独的dex文件。这样就能够防止了LoadBugClass类被打上CLASS_ISPREVERIFIED的标志了,那咱们如今来开始作这件事情。

  • 一、动态被注入类的制做

    • a、新建一个hackdex的Module,我这里来自HotFix的源码,你也能够本身新建

cl25
cl25

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

    ```
    package dodola.hackdex;

/**

  • Created by sunpengfei on 15/11/3.
    */
    public class AntilazyLoad {
    }
    ```
    c、打包成单独的dex文件,打包步骤彻底等同于补丁包的制做,因此我这里就不在走这个过程了,而后把它放置在assets下

cl26
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这货是个好东西啊,它能够以无侵入的方式重构你的原代码。我以前编写过另一个三剑客之一的文章,原理基本同样。参考地址:blog.csdn.net/xinanheisha…

步骤
  • a、建立buildSrc模块,这个项目是使用Groovy开发的,听说这货具有Java, JavaScript, Phython, Ruby等等语言的优势,并且Groovy依赖于Java的,和Java无缝挂接的

    • 你能够到这里下载SDK:groovy-lang.org/download.ht…
    • 而后,配置path环境变量,Groovy的安装挺简单的,基本上和JDK的安装差很少
    • 固然,这是Groovy自带的最基本的开发工具,你能够查看它如何支持as的
    • 若是是eclipse的话选择菜单项“Help->Install New Software”以后重启eclipse工具便可利用eclipse开发Groovy应用程序了
    • 可是工程名必定要叫”buildSrc”,这里我就直接使用了HotFix,你也能够本身构建,若你以为闲麻烦,也能够下载个人demo里面获取。

    cl27
    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](http://upload-images.jianshu.io/upload_images/4614633-fa0bae9c8d2cd99e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

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](http://upload-images.jianshu.io/upload_images/4614633-480c93281a78a0ca?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

在配置一下,侵入时期

![cl30](http://upload-images.jianshu.io/upload_images/4614633-788d25a94c10e7ad?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

ok,总算把整个过程写完了,准备开始运行了,无论你激不激动,反正本人是挺激动的了。在运行以前,先看一下咱们的引用者类

![cl31](http://upload-images.jianshu.io/upload_images/4614633-0e9fd52e4d9d09b4?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
没错,能够确认这是咱们的源代码,化成灰我也能够认出它来。在看一下运行以后的引用者类

![cl32](http://upload-images.jianshu.io/upload_images/4614633-5fe92167e9cd0644?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

项目讲解到这里,我想估计没有几我的能有耐心的看到这里来了,由于以为文章实在太长,须要有多大耐心才能扛到这里,连我本身也怀疑本身如何写出来的,不过我认为,这么强大并且实用的技术点,不是可以三五两语就能说清的,咱们要有足够的耐心来探索咱们所不知的,有耐心,咱们就有但愿,有但愿就不会失望!

ok,咱们见证一下奇迹。

![cl33](http://upload-images.jianshu.io/upload_images/4614633-dbffea3064afe73e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

看到这效果,我手已累,键盘已坏。。。。
>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
cl34

若是你已经准备好足够信心的话,能够按照文章,本身尝试一方

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

原文地址

blog.csdn.net/xinanheisha…

项目相关:

相关demo下载地址:

download.csdn.net/download/xi…

若是你以为此文对您有所帮助,欢迎入群 QQ交流群 :232203809
微信公众号:终端研发部

技术+职场
技术+职场
相关文章
相关标签/搜索