Android 热修复介绍之代码修复

什么是Android热修复技术

简单来说就是不重新安装apk的情况下,通过补丁,修复bug


正常开发流程

热修复开发流程

目前主流的热修复技术框架

  • 阿里系的: Andfix、Hotfix、Sophix
  • 腾讯系的:QQ空间超级补丁技术、Qfix、Tinker(微信)
  • 美团系的:Robust
  • 饿了么的:Amigo

关于热修复的技术积淀

  • 最开始 ,是手淘基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java method Hook技术——Dexposed。但是这个方案由于对底层Dalvik结构过于依赖,最终无法兼容Android5.0以后
  • 后来支付宝提出了新的热修复方案Andfix。Andfix同样是一种底层结构替换的方案,也达到了运行时生效及时修复的效果,阿里后来对Andfix改进,对相关业务解耦后,推出了阿里百川Hotfix方案,此时的修复已经非常的不错,对代码修复需求都可以解决,而且全版本兼容,但是问题在于Anfix本身有局限,它只提供代码层面的修复,对于资源和so库的修复都还未能实现
  • 最终在2017年Sophix的横空出世,打破了各家热修复技术纷争的局面。在代码修复,资源修复,so修复的方面,以及方案的安全性,易用性放慢,sophix都做到了业界领先

本文重点介绍如何在项目中实现代码修复

通过类加载机制实现
  • 优点:适用性强、修复范围广、限制少
  • 缺点:属于热修复中的冷修复、需要重启App
通过底层替换方法实现
  • 优点:时效好、不需重启,即使生效
  • 缺点:受限制较多(需要修改虚拟机字段,如果手机厂商修改了虚拟机…….)

ClassLoader 简介

对于 Java 程序来说,编写程序就是编写类,运行程序也就是运行类(编译得到的 class 文件),其中起到关键作用的就是类加载器 ClassLoader。说起类加载器我就想到ClassLoader的双亲委托加载机制,接下来就介绍一下类加载的双亲机制

双亲机制

当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程,具体的加载过程如下:
1 源 ClassLoader 先判断该 Class 是否已加载,如果已加载,则直接返回 Class,如果没有则委托给父类加载器。

2 父类加载器判断是否加载过该 Class,如果已加载,则直接返回 Class,如果没有则委托给祖父类加载器。

3 依此类推,直到始祖类加载器(引用类加载器)。

4 始祖类加载器判断是否加载过该 Class,如果已加载,则直接返回 Class,如果没有则尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,则委托给始祖类加载器的子类加载器。

5 始祖类加载器的子类加载器尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,则委托给始祖类加载器的孙类加载器。

6 依此类推,直到源 ClassLoader。

7 源 ClassLoader 尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,源 ClassLoader 不会再委托其子类加载器,而是抛出异常。

Android 中的ClassLoader

Android 的 Dalvik/ART 虚拟机如同标准 Java 的 JVM 虚拟机一样,也是同样需要加载 class 文件到内存中来使用,但是在 ClassLoader 的加载细节上会有略微的差别。

Android的dex文件
Android 应用打包成 apk 文件时,class 文件会被打包成一个或者多个 dex 文件,Android 中的 Dalvik/ART 无法像 JVM 那样 直接 加载 class 文件和 jar 文件中的 class,需要通过 dx 工具来优化转换成 Dalvik byte code 才行,只能通过 dex 或者 包含 dex 的jar、apk 文件来加载(注意 odex 文件后缀可能是 .dex 或 .odex,也属于 dex 文件),因此 Android 中的 ClassLoader 工作就交给了 BaseDexClassLoader 来处理。

如何通过类加载机制实现

首先需要认识BaseDexClassLoaderPathClassLoaderDexClassLoader

  • PathClassLoader:系统运作,app运行时用于加载app所有需要的类。PathClassLoader 里面除了这 2 个构造方法以外就没有其他的代码了,具体的实现都是在 BaseDexClassLoader 里面,其 dexPath 比较受限制,一般是已经安装应用的 apk 文件路径

  • DexClassLoader:程序员运作,可以通过它加载我们想加载的资源,一般包括这么几种:jar、dex、apk等。

  • BaseDexClassLoader:热修复中的大Boss,PathClassLoader和DexClassLoader均继承自BaseDexClassLoader,PathClassLoader和DexClassLoader的重要方法均在其父类BaseDexClassLoader中。(因此就需要从BaseDexClassLoader入手)。

对比 PathClassLoader 只能加载已经安装应用的 dex 或 apk 文件,DexClassLoader 则没有此限制,可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。

BaseDexClassLoader查找类的源码:

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

通过源码可以看到,BaseDexClassLoader通过pathList.findClass查找类的,这里出现一个 大Boss “PathList
PathList:中保存类所有dex文件和信息,看一下它是怎么查找类的
PathList源码:

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;
    }

前方高能:

可以看到,PathList从dexElements中查找类,如果clazz != null直接return class,这就是我们可以利用的地方,从源码看,dexElements应该是个数组或者集合,设想:我们是不是可以把我们修复bug后的xx类,打包成dex,插入到dexElements的最前面,这样,系统通过PathClassLoader,查找bug类的时候,就会下找到我们的修复bug的xx类,然后直接返回,不去管后面有bug的那个xx类,达到热修复的功能

理一下我们热修复的方案
  • 修复有bug的类,生成dex补丁包;

  • 通过反射机制得到PathClassLoader的成员变量PathList字段(DexPathList的属性)(通过上面分析知道,PathList是PathClassLoader父类BaseDexClasLoader中的)

  • 然后再反射PathList获取它的dexElements字段(是一个存放dex的Element数组)

  • 将我们生成的dex补丁包,插入到dexElements的数组的最前端

项目中的实现

实现步骤

  • 编写改变前的app

  • 编写热修复需要重写生成的类

  • 生成dex补丁包,并放到服务器

  • 编写补丁检测和下载代码

  • 编写修复补丁代码(即用反射拿到dexElements数组,把dex放到有问题的类之前)

如何生成dex补丁包

用class文件生成”001dex”补丁
android在sdk/build-tools/文件件下提供了”dx”命令工具,帮助我们将class文件生成dex文件

生成方式如下:

dx –dex –output=<要生成的文件> <’class’文件路径>

例如:
dx –dex –output=001.dex …MainAtvity …Actvity2.class …People.class

核心代码

/**
 * 加载并安装补丁
 * @type {[type]}
 */
private void loadPatch(File file){
        Log.d(TAG, file.getAbsolutePath()) ;
        if(file.exists()){
            Log.d(TAG,"文件存在...") ;
        }else{
            Log.d(TAG, "文件不存在...") ;
        }
        //获取系统PathClassLoader
        PathClassLoader pLoader = (PathClassLoader) context.getClassLoader();
        //获取PathClassLoader中的PathList
        Object pPathList = getPathList(pLoader) ;
        if(pPathList == null){
            Log.d(TAG, "get PathClassLoader pathlist failed...") ;
            return ;
        }
        //加载补丁
        DexClassLoader dLoader = new DexClassLoader(file.getAbsolutePath(),optPath, null, pLoader) ;
        //获取DexClassLoader的pathLit,即BaseDexClassLoader中的pathList
        Object dPathList = getPathList(dLoader) ;
        if(dPathList == null){
            Log.d(TAG, "get DexClassLoader pathList failed...") ;
            return ;
        }
        //获取PathList和DexClassLoader的DexElements
        Object pElements = getElements(pPathList) ;
        Object dElements = getElements(dPathList) ;

        //将补丁dElements[]插入系统pElements[]的最前面
        Object newElements = insertElements(pElements, dElements) ;
        if(newElements == null){
            Log.d(TAG, "patch insert failed...") ;
            return ;
        }
        //用插入补丁后的新Elements[]替换系统Elements[]
        try {
            Field fElements = pPathList.getClass().getDeclaredField("dexElements") ;
            fElements.setAccessible(true);
            fElements.set(pPathList, newElements);
        } catch (Exception e) {
            e.printStackTrace();
            Log.d(TAG, "fixed failed....") ;
            return ;
        }
    }

    /**
     * 将补丁插入系统DexElements[]最前端,生成一个新的DexElements[]
     * @param pElements
     * @param dElements
     * @return
     */
    private Object insertElements(Object pElements, Object dElements){
        //判断是否为数组
        if(pElements.getClass().isArray() && dElements.getClass().isArray()){
            //获取数组长度
            int pLen = Array.getLength(pElements) ;
            int dLen = Array.getLength(dElements) ;
            //创建新数组
            Object newElements = Array.newInstance(pElements.getClass().getComponentType(), pLen+dLen) ;
            //循环插入
            for(int i=0; i<pLen+dLen;i++){
                if(i<dLen){
                    Array.set(newElements, i, Array.get(dElements, i));
                }else{
                    Array.set(newElements, i, Array.get(pElements, i-dLen)) ;
                }
            }
            return newElements ;
        }
        return null ;
    }

    /**
     *  获取DexElements
     * @param object
     * @return
     */
    private Object getElements(Object object){
        try {
            Class<?> c = object.getClass() ;
            Field fElements = c.getDeclaredField("dexElements") ;
            fElements.setAccessible(true);
            Object obj = fElements.get(object) ;
            return obj ;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null ;
    }

    /**
     * 通过反射机制获取PathList
     * @param loader
     * @return
     */
    private Object getPathList(BaseDexClassLoader loader){
        try {
            Class<?> c = Class.forName("dalvik.system.BaseDexClassLoader") ;
            //获取成员变量pathList
            Field fPathList = c.getDeclaredField("pathList") ;
            //抑制jvm检测访问权限
            fPathList.setAccessible(true);
            //获取成员变量pathList的值
            Object obj = fPathList.get(loader) ;
            return obj ;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null ;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容