Ali热修复Sophix原理-笔记v0.8

作者:ca88编程

小结

代码修复冷运营方案由于它的高包容性, 大约能够修复任何代码修复的光景, 可是流入前被加载的类(比如:Application类)显然是无法被修复的。 所以大家把它看作三个兜底的方案, 在万不得已走热计划可能热安插退步的景观, 最终都会走代码冷运行重启生效, 所以大家的补丁是平等套的。 具体施工方案对Dalvik下和Art下独家做了管理:

Dalvik下通过神奇的方法防止插桩, 未有带给别样类加载功效的影响。

Art下精气神儿上设想机已经支撑多dex的加载, 我们要做的独有是把补丁dex作为主dex(classes.dex卡塔尔加载而已。

  • Java API Hook通过对Android平台的设想机注入与Java反射的方法,来改换Android设想机调用函数的方法(ClassLoader),进而到达Java函数重定向的指标。仿效

3财富热修复工夫

final static 域编译

final static域首先是三个静态域,所以大家当然感到是因为会被翻译到clinit方法中,所以自然不帮忙热布置。

可是测量检验开掘,final static修饰的基本项目/String 常量类型,不可思议的居然并不曾翻译到clinit方法中。

事实上,类加载初始化dvmInitClass在执行clinit方法之前,首先会执行initSFields,
这个方法的作用主要是给static域赋予默认值。

如果是引用类型,那么默认值为NULL。

final static 修饰的原始类型 和 String 类型域(非引用类型),并不会翻译在clinit方法中,而是在类初始化执行initSFields方法时得到了初始化赋值。
final static 修饰的引用类型,初始化仍然在clinit方法中。

我们在Android品质优化的相关文书档案中时常来看,假设四个田野先生是常量,那么推荐尽量选择static final作为修饰符。

很明确那句话超小对,得到优化的无非是final static原始类型和String类型域(非征引类型卡塔尔(قطر‎,假如是引用类型,实际上是不会得到任何优化的。


final static String类型的变量,编写翻译时期会被有优化成const-string指令,可是该在指令获得的只是字符串常量在dex文件构造中字符常量区的索引id,所以供给分外的二回字符串查找。

dex文件中有一块区域存储着程序所有的字符串常量,
最终这块区域会被虚拟机完整加载到内存中,这块区域也就是通常所说的“字符串常量区”内存。

据此,大家能够获取以下结论

  • 改进final static基本项目只怕String类型域(非援用类型卡塔尔(قطر‎,由于编写翻译器间援引到基本项指标地点会被随时数替换,引用到String类型域的地点会被常量池索引id替换,所以在热安插格局下,最后具备引用到该final static域的秘诀都会被调换。实际上当时还是能走热安顿。
  • 修正final static引用类型域,是区别意的,因为那个田野同志的早先化会被翻译到clinit方法中,所以这个时候无法走热陈设。

原理:为了消除Dalvik下unexpected dex problem至极而接收插桩的章程, 单独放一个相助类在单独的dex中让别的类调用, 阻止了类被打上CLASS_ISPREVESportageIFIED标记进而隐蔽难题的面世。 最后加载补丁dex取得dexFile对象作为参数创设二个Element对象插入到dexElements数组的最前方。

Sophix同一时直接收了热运行的平底替换方案及冷运营的类加载方案,四个方案使用的补丁是一律的。优先热运营。

3.4别有风趣的资源修复方案

行使差量财富包,补丁丰裕小,不凌犯打包流程。
创设差量的0x66的补丁能源包,只含有改造之后的能源项。然后径直在原有的AssetManager中addAssetPath这些财富包。并校订补丁包中引用的财富id,旧能源id发生变化的id。

Java语言的编写翻译实现所带给的挑战

Sophix一向秉承 粒度小、重视连忙热修复、无侵入相符原生工程。因为那么些原则,大家在研究开发进度中相见不菲 编译期 的主题材料,引用影象浓烈。

插桩完成的前因后果

芸芸众生, 假设仅仅把补丁类打入补丁包中而不做别的管理的话, 那么运转时类加载的时候就能够非常退出, 接下来先来看下抛那些特别的来龙去脉。

加载二个dex文件到本地内部存款和储蓄器的时候, 若是不设有odex文件, 那么首先会实施dexopt, dexopt的进口在davilk/opt/OptMain.cpp的main方法, 最后调用到verifyAndOptimizeClass实施真正的verify/optimize操作。

图片 1

apk第四回安装的时候, 会对原dex试行dexopt, 那时假诺apk只设有一个dex, 所以dvmVerifyClass结果为true。 所以apk中全体的类都会被打上CLASS_ISPREVE索罗德IFIED标识,接下去试行dvmOptimizeClass, 类接着被打上CLASS_ISOPTIMIZED标志。

dvmVerifyClass: 类校验, 类校验的目标大概的话就是为了有备无患类被点窜校验类的合法性。 那个时候会对类的各类方法进行校验, 这里我们只须求驾驭假诺类的持有办法中央市直机关接援引到的类(第一层级关系,不会进展递归寻找)和当前类都在同三个dex中的话, dvmVerifyClass就赶回true。

dvmOptimizeClass: 类优化, 轻便的话那一个进度会把有些指令优化成设想机内部指令, 比如方法调用指令: invoke-一声令下产生了invoke--quick, quick指令会从类的vtable表中央政府机关接取, vtable简单的说正是类的保有办法的一张大表(包括世襲自父类的点子)。由此加快了法子的实行速率。

如今借使A类是补丁类, 所以补丁A类在单身的dex中。 类B中的某些方法援用到补丁类A, 所以实行到该方法会尝试深入分析类A。

图片 2

上边的代码相当的轻巧看出来, 类B由于被打上了CLASS_ISPREVE昂CoraIFIED标识, 接下来referrer是类B, resClassCheck是补丁类A, 他们归属差别的dex, 所以dvmThrowIllegalAccessError。 为驾驭决那个标题, 三个单身毫无干系帮忙类放到叁个独门的dex中, 原dex中全体类的构造函数都引用这么些类,通常的兑现格局都以侵袭dex打包流程, 利用.class字节码更正手艺, 在全数.class文件的布局函数中引用那么些扶植类, 插桩由此而来。 依据后边的介绍, dexopt进程中dvmVerifyClass类校验重临false, 原dex中负有的类都未有CLASS_ISPREVE昂CoraIFIED标记, 因而湮灭运营时那些特别。

不过插桩是会给类加载功能带给相比较严重的震慑的。 熟稔Dalvik虚构机的同桌知道, 三个类的加载平日常有多少个级次, dvmResolveClass->dvmLinkClass->dvmInitClass, 那么些多少个阶段不一一详细进行表达。 dvmInitClass阶段在类深入分析完成尝试初阶化类的时候奉行, 这些艺术首要产生父类的早先化,当前类的开头化, static变量的初阶化赋值等等操作。

图片 3

能够观察除了上面说的类开头化之外, 假诺类没被打上CLASS_ISPREVERIFIED/CLASS_ISOPTIMIZED标识, 那么类的Verify和Optimize都就要类的伊始化阶段张开。 不奇怪情形下类的Verify和Optimize都仅仅只是在apk第三遍安装实践dexopt的时候进行, 类的Verify实际上是比较重的, 因为会对类的享有办法中的全部指令都开展校验, 单个类加载来看类Verify并不耗费时间, 但是一旦同时点加载大量类的意况下, 这几个耗费时间就能够被加大。 所以这也是插桩给类的加载效能带给很大影响的结果, 接下来来看下具体会给类加载带给多大的熏陶。

长远搜求Android热修复技艺原理那本书注重讲明了Android的热修复中的热安顿,冷布置以至财富和so库的修复本事。全文主要讲Sophix应对以上八个地方的能力解析,不管是本人产物恐怕产业界别的方案的横纵相比,Sophix技巧这两天都以最优的。

1.热修复本事介绍

静态域/静态代码块

实际,热铺排方案除了不援助method/田野的新添,同期也是不援助<clinit>的修复,因为那几个方法是dalvik虚构机中类举办最初化的时候调用。

在Java源码中并不曾clinit那一个主意,这几个措施是android编写翻译器自动合成的点子。

通过测量试验开掘,静态田野同志的伊始化和静态代码块实际上都会被编译器便已在<clinit>那一个主意。

静态代码块和静态域初叶化在clinit中的前后相继关系正是相互出今后源码中的前后相继关系。

类加载然后开展类伊始化的时候,会去调用clinit方法,三个类仅加载一遍。

在下面三种情况下,会尝试加载一个类:(在面试中被问到过)
1. new一个类的对象;
2. 调用类的静态方法;
3. 获取类静态域的值;

首先判断这个类有没有被加载过,如果没有,执行的流程是`dvmResolveClass -> dvmLinkClass -> dvmInitClass `。 
类的初始化是在dvmInitClass。这个函数会首先尝试对父类进行初始化,然后调用本类的clinit方法。

唯其如此说的任何点

咱俩理解DexFile.loadDex尝试把叁个dex文件解析并加载到native内部存储器, 在加载到native内部存储器早先, 假诺dex不设有对应的odex, 那么Dalvik下会实践dexopt, Art下会实行dexoat, 最后收获的都是三个优化后的odex。 实际上末了设想机实施的是这些odex实际不是dex。

现行反革命有那样三个标题,若是dex丰盛大那么dexopt/dexoat实际上是很耗费时间的,办事处方我们提到的方案, Dalvik下实际影响很小, 因为loadDex仅仅是补丁包。 不过Art下影响是相当大的, 因为loadDex是补丁dex和apk中原dex合併成的八个完全补丁压缩包, 所以dexoat非常耗费时间。 所以假如优化后的odex文件没变化照旧没生成三个全部的odex文件, 那么loadDex便不能够在运用运转的时候进行的, 因为会卡住loadDex线程, 日常是主线程。 所以为了湮灭那一个标题, 大家把loadDex充任一个作业来看, 即便中途被打断, 那么就删除odex文件, 重启的时候借使开掘成在odex文件, loadDex完之后, 反射注入/替换dexElements数组, 完毕patch。 倘诺子虚乌有odex文件, 那么重启另三个子线程loadDex, 重启之后再生效。

别的一端为了patch补丁的安全性, 即便对补丁包举办签订协议校验, 那时候可防止止全部补丁包被篡改, 但是实际因为设想机实施的是odex并不是dex, 还亟需对odex文件进行md5完整性校验, 借使相配, 则直接加载。 不相配,则再度生成壹回odex文件, 幸免odex文件被曲解。

在事变分发流中,通过Hook钩子在事件传送到极点前截获并监察和控制事件的传输,进而管理部分特定干预事件。

2.2您所不亮堂的Java

代码修复技能详细解释

冷运营重启生效,未来相符有以下三种完结方案, 同期提交他们分别的得失:

  1. 补丁小,合成不占太多空间和总体性。
  2. 对代码的侵入小,对native代码的hook也不难,做到最大宽容。
  3. 协理的修复范围广。支持小范围的即时生效和大面积的冷运行。也扶助so库和能源修复。

一种新的全量dex方案

在基线包里去除掉补丁包里的class后,然后将此外的dex都load进来。基线包能够找到补丁中新类,补丁包中新类也能够找到基线包中不改变的类。那样基线包中不改变的class依然能够固守旧的逻辑odex,最大程度保障了dexopt的效能。
在dex文件中类的进口在DexHeader中彰显为class_defs。遍历pHeader->classDefsOff偏移值获取DexClassDef,借使发掘这一个类名存在补丁中就从pClassDefs数组中移除,重新排列,改进classDefsSize。那样校勘不去移除类的概念等新闻,纵然会残余类等消息,可是会巩固dex的管理速度。

泛型编写翻译

泛型是java5才初始引进的。泛型的利用也或许会产生method的激增。

Java语言的泛型基本上都以在编译器中得以达成的。

由编写翻译器奉行项目检查和种类估计,然后生成普通的非泛型的字节码,正是虚拟机完全无感知泛型的存在。

这种达成技能造成擦除(erasure)。编写翻译器使用泛型类型音讯保障项目安全,然后在生成字节码早先将其清除。由于泛型是在java 5中才引进的,扩展设想机指令集来帮助泛型是令人不大概经受的,因为那会为Java商家进级其JVM产生难以超越的障碍,由此才使用了可以完全在编写翻译器中贯彻的擦除方法。


项目擦除与多态的冲突

class A<T> {
    private T t;
    public T get() {
        return t;
    }
    public void set(T t) {
        this.t = t;
    }
}

class B extends A<Number> {
    private Number n;

    @Override  // 跟父类返回值不一样,为什么重写父类get方法?
    public Number get() {
        return n;
    }

    @Override  // 跟父类方法参数不一样,为什么重写set方法?
    public void set(Number n) {
        this.n = n;
    }
}

class C extends A {
    private Number n;

    @Override  // 跟父类返回值不一样,为什么**重写**父类get方法?
    public Number get() {
        return n;
    }
    @Override  // 跟父类方法参数不一样,为什么**重载**set方法?
    public void set(Number n) {
        this.n = n;
    }
}

缘何类B的set和get方法能够用@Override而不报错。

@Override申明那么些点子是重写,大家精通重写的意味是子类中的方法签字和重回类型都一定要一律。

但是很鲜明的,B的方法不恐怕对A的set/get方法开展重写的。其实大家的原意是重写完成多态,不过类型擦除后,只好产生了重载。

诸如此比,类型擦除就和多态有了冲突。

实际JVM选拔了三个异样的形式,来产生重写那些职能,那正是bridage方法。

.method public get() Ljava/lang/Number;
.method public bridge synthetic get() Ljava/lang/Object;
    invoke-virtual {p0}, Lcom/taobao/test/B;->get()Ljava/lang/Number;
    move-result-object v0
    return-object v0
.end method

.method public set(Ljava/lang/Number;) V
.method public bridge synthetic set(Ljava/lang/Object;) V
    check-cast p1, Ljava/lang/Number;
    invoke-virtual {p0, p1}, Lcom/taobao/test/B;->set(Ljava/lang/Number;)V
    return void
.end method

大家开采访编辑写翻译器会自动生成七个bridage方法来重写父类方法,同偶尔间那五个章程其实调用B.set(Ljava/lang/Number;)B.get()Ljava/lang/Number那五个重载方法。

子类中真正重写基类方法的是编写翻译器自动合成的bridge方法。而类B定义的get和set方法方面包车型客车@Override只但是是假象。设想机美妙的使用桥方法的秘技来减轻了项目擦除和多态的冲突。

也正是说,类B中的字节码中get(卡塔尔国Ljava/lang/Number;和get(0Ljava/lang/Object;是还要设有的,那就颠覆了作者们的认识,因为在寻常代码中他们是不可能同不经常间存在的。

据此,设想机为了完结泛型的多态做了二个看起来“不合规”的事情,然后交由设想机本人去分别管理了。


方案1 强迫绕过类Verify阶段

抑遏hook Dalvik设想机的dvmVerifyClass函数,让其直接重回true,进而绕过加载的时候不供给的校验机制,进而落成加速利用的开发银行速度的指标。 实际上集团安全体已经有那样的方案。 具体参照他事他说加以考查: dalvikUpSpeed本领介绍--加速android移动端低档机的启航品质

但是这种方案也设有分明的缺点: 此时native hook的是贰个事关dalvik根基功用而且调用很频仍的格局,无疑恐怕存在一点都超大的风险。 其它一端这么些依然必要插桩的, 须要侵入打包流程, 打包时修正.class字节码文件, 由于大家热修复的基调是一心不侵略打包流程, 所以须求寻求其它一种更文雅的技术方案。

类加载方案
  • 原理:让app重新起动后让ClassLoader去加载新的类。要是不重启,原来的类还在设想机中不能再度加载。

  • 亮点:修复范围广,约束少。

  • 使用:Tencent系满含QQ空间,手QFix,Tinker选取此方案。QQ空间会侵入打包流程。QFix须要获得底层设想机的函数,不安静。Tinker是全部的全量dex加载。

dex的大大小小占整个apk比例非常的低,三个app里的dex文件大小不是非同通常部分,占空间大的要紧是财富文件。

图片 4冷运营主流框架解析

  • QQ空间的插桩原理将三个单身毫无干系版主类放到二个单身的dex中,原dex中全体类的布局函数都援用这一个类,日常的得以达成格局都以侵袭dex打包流程,利用.class字节码校正手艺,在全体.class文件的布局函数中援引这几个援救类。

  • Tinker与Sophix方案区别之处Tinker选用dex merge生成全量DEX方案。反编写翻译为smali,然后新apk跟基线apk举行差距相比较,最终收获补丁包。Dalvik下Sophix和Tinker相近,在Art下,Sophix无需做dex merge,因为Art下精气神上虚构机已经支撑多dex的加载,要做的仅仅是把补丁dex作为主dex(classes.dex卡塔尔(قطر‎加载而已:将补丁dex命名叫classes.dex,原apk中的dex依次命名叫classes(2, 3, 4...卡塔尔(قطر‎.dex就好了,然后一同装进为三个压缩文件。然后DexFile.loadDex获得DexFile对象,最后把该DexFile对象整个替换旧的dexElements数组就好了。

    图片 5Tinker与Sophix方案不一样点

DexFile.loadDex流程DexFile.loadDex尝试把叁个dex文件剖析并加载到native内部存款和储蓄器,在加载到native内存在此以前,假如dex荒诞不经对应的odex,那么Dalvik下会实行dexopt,Art下会进行dexoat,最终获得的都以一个优化后的odex。实际上最后虚构机上施行的是那么些odex并非dex。

图片 6dexopt流程

骨干参照InstantRun的完毕:构造一个带有全部新能源的新的AssetManager。并在具有在此之前援引到原来的AssetManager通过反射替换掉。Sophix不校订AssetManager的援引,布局的补丁包中只包罗有新添或有校订变动的能源,在原AssetManager中addAssetPath这几个包就能够了。能源包无需在运转时合成完整包。

真相是对native方法的修补和替换。相似类修复反射注入格局,将补丁so库的门径插入到nativeLibraryDirectories数据最前头。

热修复主流框架相比较可查阅Android热修复主流框架应用商讨

switch case语句编写翻译

  • switch case语句编写翻译法规
    编译器会依赖switch case的值是不是接二连三分别生成不一样的指令,packed-switch和sparse-switch指令。假如packed有值不总是就用pswitch_0补齐 return-void。
  • 热铺排施工方案
    在sophix实行财富补丁包时,必要对援引的能源开展替换,若是swith case语句适逢其会被编写翻译成packed-switch指令则恐怕会窥豹一斑。解决情势是校订打补丁包时的smail反编写翻译流程,遭遇packed-switch指令强转为sparse-switch指令,:pswitch_N等标签指令也急需被替换到:sswitch_N指令,然后做财富Id替换,编制程序smail为dex

Lambda表明式编写翻译

Lambda表明式是 java 7才引进的一种表明式,相仿于匿名内部类实际上由于佚名内部类有十分的大的界别。

Lambda表明式的接受也或者招致方法的疯长/收缩,以致最后走持续热陈设格局。

lambda为Java添加了缺失的函数式编程特点,Java现在提供的最接近闭包的概念便是Lambda表达式。

Java编译器将lambda表达式编译成类的私有方法,使用了Java7的invokedynamic字节码来动态绑定这个方法。

在Java 7 JVM中增加了一个新的指令invokedynamic,用于支持动态语言。
即允许方法调用可以在运行时指定类和方法,不必在编译的时候确定。

字节码中每条invokedynamic指令出现的位置称为一个动态联调点,
invokedynamic指令后面都会跟一个指向常量池的调用点限定符(#3, #6),这个限定符会被解析成一个动态调用点。

热安排应对方案

  • 日增/收缩四个Lambda表明式会以致类方法相比较散乱,所以都会促成热铺排失败
  • 改良一个Lambda表明式基于前边的深入分析,可能会促成新扩大田野同志,所以这时候也会导致热安插失利。

Art下冷运行实现

前方说过补丁热安排形式下是三个安然如故的类, 补丁的粒度是类。 未来大家的需若是补丁不仅能走热安排形式也能走冷运行情势, 为了减小补丁包的深浅, 并未为热安排和冷运维分别思谋一套补丁, 而是同一个热布署方式下的补丁能够降级直接走冷运维, 所以大家无需做dex merge。 不过后面大家清楚为了消除Art下类地址写死的难题, tinker通过dex merge成一个簇新完整的新dex整个替换掉旧的dexElements数组。 事实上我们并不须要那样做, Art设想机上面私下认可已经帮忙多dex压缩文件的加载了。

咱俩独家来看下Dalvik下和阿特下对DexFile.loadDex尝试把一个dex文件分析加载到native内部存款和储蓄器都发生了什么样,实际上都以调用了DexFile.openDexFileNative这几个native方法。 看下Native层对应的c/c 代码具体贯彻。

Dalvik虚构机下边:

图片 7

static const char* kDexInJarName = "classes.dex"; 很显著Dalvik尝试加载二个压缩文件的时候只会去把classes.dex加载到内部存款和储蓄器中... 假如此刻压缩文件中有多dex, 那么除了classes.dex之外的别的dex被向来忽视掉。

阿特设想机上边: 方法调用链DexFile_openDexFileNative-> OpenDexFilesFromOat -> LoadDexFiles

图片 8

地点代码大家大要能够看出来Art上边暗许已经帮忙加载压缩文件中隐含八个dex, 首先确定优先加载primary dex其实便是classes.dex, 后续会加载其余的dex, 所以补丁类只需求停放classes.dex就能够。 后续出现在任何dex中的"补丁类"是不会被再度加载的。 所以我们收获Art下最终的冷运行实施方案: 我们假使把补丁dex命名称为classes.dex. 原apk中的dex依次命名字为classes.dex就好了, 然后一并装进为四个压缩文件, 然后DexFile.loadDex获得DexFile对象, 最终把该DexFile对象整个替换旧的dexElements数组就足以了。

一张图来看下我们的方案和方案二的例外:

图片 9

亟需在意一点:

补丁dex必需命名称叫classes.dex

loadDex获得的DexFile完整替换掉dexElements数组并不是插入

底层替换方案
  • 规律:在早已加载的类中平素沟通掉原有艺术,是在原有类的组织幼功上拓宽改进的。在hook方法入口ArtMethod时,通过组织贰个新的ArtMethod落成替换方法入口的跳转。
  • 采纳:能即时生效,Andfix采取此方案。
  • 破绽:底层替换牢固性不佳,适用范围存在节制,通过改建代码绕过限定既不文雅也不低价,並且还未提供资源及so的修补。

dvmOptResolveClass难点与方针

清除标记遇到问题。多dex应用时,如果在Application类没有被打上pre_verified标记,那么虚拟机在初始化类时会扫描其所有用到的类进行dvmOptResolveClass操作,这个方法对类进行初始化加载的是原dex的类,那么这些类就会被打上pre_verified标记。补丁加载之后,只对Application类进行了清除标记操作。那些打上标记的类调用补丁类的话还是会出现pre_verified问题。
绕过dvmOptResovleClass操作,仅仅让Application类被打上标记的解决方案
  • 让Application用到的非系统类和Application在同八个dex中,保证打上pre_verified标识,幸免步向dvmOptResolveClass操作。(Android官方的multi-dex就是将Application用到的类都打包到主dex中)
  • 让Application类除了热修复代码之外,别的代码抽离开放到三个单身的类。Application通过反射调用别的类,是的Application彻底与任何类独立开,有限支撑被打上pre_verified标记。

内部类

难题:一时候会发觉,修正外界类有个别方法逻辑为访谈内部类的某些方法时,最终打包出来的补丁包竟然提醒新扩充了二个方法。

由此大家很有必不可缺驾驭在那之中类在编写翻译时期是怎么编写翻译的。

第一须求理解 ** 内部类会在编写翻译期会被编写翻译为跟外界类相符的一流类。 **


静态内部类和非静态内部类的差距。

它们的区分其实大家都很熟悉,非静态类持有外界类的引用,静态内部类不有所外界类的援引。

既是内部类跟外界类同样都是顶尖类,那是或不是表示对方private的method/田野先生是可望而不可及被访问到的,事实上国外国语大学部类为了访问内部类民用的域和章程,编写翻译期会自动外内部类生成access&**有关措施。

为此,要是补丁类中期维修改的情势中添加了急需拜望内部类私有多少大概措施的代码的话,那么编写翻译时期会陡增access&**主意,供内部类被访谈使用。


设若想透过热铺排修复的新情势需求拜谒内部类的私有域或方式,那么大家应该防止生成access&**连带办法。

Sophix有以下提议:

  • 表面类纵然有在这之中类,把外界类具备的method/fidle的private采访权限改良为projected或许私下认可访谈权限或许public。
  • 还要把内部类的具有的method/田野同志的private访问更正为projected或然格局访谈权限或然public。

方案二

原理:提供dex差量包, 全部替换dex的方案。 差量的法子给出patch.dex, 然后将patch.dex与应用的classes.dex合併成一个完全的dex, 完整dex加载获得的dexFile对象作为参数创设一个Element对象然后一体化替换掉旧的dexElements数组。

优点:自行研制dex差距算法, 补丁包一点都不大, dex merge成完整dex, Dalvik不影响类加载质量, Art下也不设有必得包括父类/引用类的事态;

缺点:dex归并内部存款和储蓄器消耗在vm heap上, 轻松OOM, 最终造成dex合併倒闭。

咱俩能清楚的观察七个方案的弱项都很明确。 这里对tinker方案dex merge缺欠进行简短说美赞臣下: dex merge操作是在java层面实行,全部指标的分红都以在java heap上, 若是当时历程申请的java heap对象超越了vm heap规定的高低, 那么进度产生OOM, 那么系统memory killer恐怕会杀掉该进度, 导致dex合成失利。 其余一边大家精通jni层面C new/malloc申请的内部存款和储蓄器, 分配在native heap, native heap的增加并不受vm heap大小的范围, 只受限于RAM, 假设RAM不足那么进程也会被杀死引致闪退。 所以假使只是从dex merge方面思谋,在jni层面进行dex merge, 进而可避防止OOM提升dex合併的成功率。 理论上圈套然能够,只是jni层达成起来比较复杂而已。

作品的开始大家说过, 大家的要求是冷运维方式是热铺排情势的补偿兜底方案, 所以那多少个方案使用的应有是均等套补丁, 别的一个方面跟代码修复热计划方案同样, 我们追求的是不入侵打包。 上述二种方案都急需侵入应用打包进度, 同一时间补丁的架构也不近似, 这两套方案对我们来讲皆以不适用。 所以大家供给风格迥异冷运维修复, 寻求一种不仅可以无侵入打包又能做热安顿情势下兜底补充的减轻方案, 下边将对Dalvik设想机和Art虚拟机的冷运转方案分别进行介绍。

图片 10sophix与主流框架相比较

2.4多态对冷运营加载的熏陶

防止插桩

方案2 雅淡实现幸免插桩

手Q热补丁轻量级方案给了小编们贯彻的笔触, 不问可以知道:

图片 11

怎么让dvmDexGetResolvedClass重返的结果不为null,只要调用过贰遍dvmDexSetResolvedClass(pDvmDex, classIdx, resClass);就能够了,举个例证轻易表明下。

图片 12

笔者们当时亟需patch的类是类A, 所以类A被打入到多少个独立的补丁dex中。那么试行到类B的test方法时, 实施到A.a(卡塔尔(قطر‎那行代码时就能够尝试去深入深入分析类A, 那个时候dvmResolveClass(const ClassObject* referrer, u4 classIdx, bool fromUnverifiedConstant)

referrer: 实际上正是类B

classIdx:类A在原dex文件构造类区中的索引id

fromUnverifiedConstant: 是否const-class/instance-of指令

那时候是调用的是A的静态a方法, invoke-static指令不归属const-class/instance-of那五个指令中的一个。 不做任哪儿理的话, dvmDexGetResolvedClass一开头是null的。 然后A是从补丁dex中深入分析加载, B是在原Dex中, A在补丁dex中, 所以B->pDvmDex != A->pDvmDex, 接下来施行到dvmThrowIllegalAccessError进而形成运维时特别。 所以大家要做的是, 必定要在一同来的时候, 就把补丁A类增添到原来dex的pResClasses数组中。 那样就确认保证了施行B类test方法的时候, dvmDexGetResolvedClass不为null, 就不会实行前边类A和类B的dex一致性校验了。

实际落到实处, 首先大家经过补丁工具反编写翻译dex为smali文件获得:

preResolveClz: 须要patch的类A的描述符, 非必得, 为了调整方便加上该参数而已. --> Lcom/taobao/patch/demo/A;

refererClz: 须要patch的类A所在的dex的别的一个类描述符, 注意这里不限定必需是援用补丁类A的某部类, 实际上假若同三个dex中的任何一个类都足以。 所以大家一向拿原dex中的第贰个类就能够. --> Landroid/support/annotation/AnimRes;

classIdx: 必要patch的类A在本来dex文件中的类索引id. --> 2425

下一场经过dlopen得到libdvm.so库的句柄, 然后经过dlsym得到该so库的dvmResolveClass/dvmFindLoadedClass函数指针。 首先要求预加载援用类->android/support/annotation/AnimRes, 那样dvmFindLoadedClass("android/support/annotation/AnimRes"卡塔尔才不为null, dvmFindLoadedClass推行结果获得的ClassObject做为第二个参数实践dvmResolveClass(AnimRes, 2425, true卡塔尔国就能够。

回顾看下JNI层代码部分完结。 实际上能够看出preResolveClz参数是非必得的。

图片 13

圆满解决。 这一个思路与前边方案一的native hook格局各异,不会去hook有个别系统方法,而是从native层直接调用, 同期更无需插桩。 具体得以达成必要注意以下三点:

dvmResolveClass的第多个参数fromUnverifiedConstant必需为true。

apk多dex意况下,dvmResolveClass第一个参数referrer类必需跟要求patch的类在同一个dex, 不过他俩多个类无需存在别的引用关系,任何八个在同叁个dex中的类作为referrer都得以。

referrer类必需超前加载。

热修复框架集成文书档案

sophix集成
tinker集成

立马生效带给的范围

除了这几个之外反射的主题材料,即时生效直接在运营期改良底层结构的热修复方法,都设有着一个范围,这正是一定要替换方法。对于补丁里面假如存在方法的充实照旧减削,以致成员字段的增添和裁减情形都以不适用的。

缘由是如此的,一旦补丁类中现身了主意的增减,会变成整个类甚至任何dex方法数的退换。方法数的更改伴随着艺术索引的浮动,那样在探问时就不能够所指引精确的章程了。

若果字段发生了增减,和格局变化大致,全部字段的目录都会产生变化。

与此同不常间进一层严重的是,假诺程序运转中间有些类忽然扩大了八个字段,那么对于本来已经转移的实例,照旧原先的布局,已经是不能改造的了,而新点子在使用到敦厚例时,访问新添字段就可以发出不可预料的结果。

故此综合来讲,即时生效方案唯有在底下三种状态下是不适用的:

  1. 引起了原有类产生布局转变的改变
  2. 修复了的非静态类会被反射调用

虽说有一对施用约束,但假设满足使用标准,这种热修复形式照旧那些名列前茅的,补丁小,加载高效,能够实时生效,无需重启app,况兼有所完美的配备宽容性(整个copy Method结构)。

插桩引致类加载品质影响

图片 14

上一小节的牵线, 大家精晓若使用插桩招致全部类都非preverify,这变成verify与optimize操作会在加载类时接触。 那就可以促成类加载有一定的性质损耗,Wechat做过三遍测量试验, 分别选取优化和不优化二种方法做过三种测量试验, 分别使用插桩与不插桩二种办法开展二种测量检验,一是三回九转加载700个50行左右的类,一是总结应用运转实现的漫天耗费时间。

图片 15

平均每一个类verify optimize的耗费时间并非常的短,况兼以此耗费时间每一种类只有一回。但出于使用刚运行时这种景色下平日会同期加载多量的类,在这里个情景影响如故一点都不小的, 运行的时候就便于白屏, 那点是出于无奈忍受的。

有意思的域编译

  • 静态field,非静态field编译
    热安排不援助田野(field卡塔尔国/method增删和<clinit>方法的改造
    静态田野(field卡塔尔(قطر‎的初阶化和静态代码块会被编写翻译在编写翻译器合成的秘技<clinit>中
    非静态字段的初叶化会被编写翻译在编写翻译器生成的<init>无参构造函数中
  • 静态田野同志,静态代码块
    <clinit>方法会在类加载阶段的类初阶化时调用,<clinit>中静态田野(field卡塔尔和静态代码块的面世顺序就是两个在源码中现身的一一。因为类已经加载过了,所以即便修复了<clinit>方法也不会生效了。
    dvmResolveClass->dvmLinkClass->dvmInitClass,然后施行clinit方法
    以下境况会去加载三个类
    1.new 三个类的对象时new instance
    2.调用类的静态方法(invoke static)
    3.获取类的静态域的值(sget)
  • 非静态田野先生,非静态代码块
    类的布局函数会被编写翻译器翻译成<init>方法,会先进行非静态田野先生和非静态代码块的开首化。它们现身的逐个也是和在源码中冒出的一一相通。
    进行new instance指令时,假设类未有加载过,就尝试加载类。然后对目的内部存款和储蓄器分配,再然后实施invoke direct指令调用类的init构造函数进行初阶化
  • 热陈设实施方案
    不帮衬对静态字段和静态代码块的纠正,会促成热布置失败,只可以冷运维生效。协理非静态字段和非静态代码块改过,热安排只是将init布局函数作为日常的主意改变。

办法混淆

实在不外乎上边提到的内部类/无名氏内部类大概会以致method新添之后,代码混淆也大概会导致方法的内联和剪裁,那么最后大概也会促成method的增加生产数量/降低。

实在如若混淆配置文件加上-dontoptimize那项就不会去做方法的剪裁和内联。

雷同意况下项目标混淆配置都会动用到android sdk暗许的模糊配置文件proguard-android-optimize.txt或者 proguard-android.txt,两者的区分正是后人应用了-dontoptimize这一项配置,而后边三个未有采纳。

图片 16

ProGuard_build_process

实在,图上的多少个步骤都以足以选拔的,此中对热安顿也许会时有产生严重影响的主要在optimization阶段。

optimization step: 进一层优化代码,不是入口点的类和方式能够被设置成private、static或final,无用的参数可能会被有移除,並且有的地点恐怕会被内联。

能够见见optimization阶段,除了会做方法的剪裁和内联或然引致方法的新扩展/收缩之外,还或许把办法的修饰符优化成 private/static/final。热补丁安顿方式下,混淆配置最好都抬高-dontoptimize配置。

`` : 针对.class文件的预校验,在.class文件中丰硕StackMa/StackMapTable消息,那样Hotspot VM在类加载时候实行类校验阶段会省去一些手续,因而类加载将更加快。

大家掌握android设想机施行的dex文件,编写翻译时期dx工具会把具有的.class文件优化成.dex文件,所以混淆库的预校验在android中是还未别的意义的,反而会拖累打包速度。

android虚构机中有和煦的一套代码校验逻辑(dvmVerifyClassState of Qatar。所以android中模糊配置经常都亟需丰裕-dontpreverify配置。

缺点:Dalvik下影响类加载品质,阿特下类地址写死, 招致必须含有父类/引用, 最终补丁包超级大。dex合併内部存款和储蓄器消耗在vm heap上, 轻易OOM, 最终引致dex归总倒闭。

冷运转类加载修复

QQ空间

  • dex插入方案,将补丁dex插入到classLoader索引路线最前头
  • 通过插桩情势绕过Dalvik虚构机下类的pre-verify难点

QFix

  • 同是dex插入方案
  • 经过在Jni层提前resolve全数补丁类绕过pre-verify检查

Tinker:

  • 全量合成新dex,消亡重复class重复带来的冲突
  • 怀有类加载都在一个dex完毕,未有pre-verify难点

虚构机调用方法的准则分析

怎么替换阿特Method数据后得以达成热修复呢?那亟需从虚拟机调用方法的原理聊到。

以Android6.0达成为例。

ArtMethod构造中最根本的三个字段entry_point_from_interprete_entry_point_from_quick_compiled_code_

ART中得以接纳解释形式或许AOT机器码情势进行

解释模式:
就是取出Dex Code,逐条的解释执行就行了。如果方法的调用者是以解释模式运行的,在调用这个方法是,就去取得这个方法的`entry_potin_from_interpreter_`,然后跳转过去执行。

AOT模式:
预先编译好Dex Code对应的机器码,然后运行期直接执行机器码就行了,不需要一条条的解释执行Dex Code。如果方法的调用者是以AOT机器码执行的,在调用这个方法是,就是跳转到`entry_point_from_quick_compiled_code_`执行。

那大家是还是不是只需求替换那多少个字段就能够了呢?

并不曾这么轻松。认为无论是演说方式只怕AOT方式,在运营时期还有恐怕会需求使用ArtMethod的内部的此外成员字段的。

骨子里那样正式native替换形式包容性难题的案由。

Sophix未有选拔将Artmethod的全部成员都实行更换,而是把ArtMethod作为全日进行替换。

此处要求重申的是求解阿特Method布局的内部存款和储蓄器占用大小。

由于大家是在运行时生效(各家ROM都会有多多少少的转移),且sizeofsizeof()办事规律是在编译期,因而大家爱莫能助直接动用该表明式。

Sophix采纳了相比聪明的方法:利用现行反革命构造的一定,使用七个ArtMethod早前的偏移量来动态总结ArtMethod的数据布局大小。但这里必要依附存放ArtMethod的数据构造是线性的。

提供dex差量包, 全部替换dex的方案。 差量的方式给出patch.dex, 然后将patch.dex与使用的classes.dex合併成二个平安无事的dex, 完整dex加载取得的dexFile对象作为参数创设一个Element对象然后一体化替换掉旧的dexElements数组。

拉姆da表达式编写翻译

  • 拉姆da表达式编写翻译准绳
    Lamda表明式具备函数式编制程序的性状,是Java中最相符闭包的概念。函数式接口:叁个接口具备独一七个虚无方法
    Java中的Runable和Comparator都是规范的函数式接口
    Lamada表达式和无名氏内部类的差别:
    1.this第一字指包围Lamada表明式的类而不是指向无名内部类自个儿
    2.编译形式,Java编写翻译器将Lamda表明式编译成类的个人方法,使用了Java7的invokedynamic动态绑定这几个私有方法。而无名氏内部类则是生成外界类&number的新类
    编译器都会在类下生成lamda$main$**{ * }私有静态方法,这一个艺术达成了lamda表达式的逻辑,引用的变量都会化为方法的参数。
    在HostSpot VM下解释class文件的lamda表达式:
    invokeDynamic指令调用java/lang/invoke/LamdaMetafactory的metafactory这么些静态方法。这一个方法会在运转时生成达成函数式接口的具体类,这一个具体类会调用这几个静态私有方法。
    在Android虚构机下解释dex文件中的lamda表达式:则是在优化成dex文件的时候就生成了这几个具体类。
  • 热布署设计方案
    新增加lamada表明式会导致外界类新扩展七个推来推去方法。修改的lamda表明式逻辑引用了外界变量,会变成协助类持有了表直面象,会剧增这么些外界对象的变量。也是会以致热修复失利。

轮番后措施访谈权限难点

1、类内部

上述提到,我们所有的事替换阿特Method的源委,但新替换的办法的所属类和原本方式的所属类,是见智见仁的类。

被替换的措施有权力访问别的的private方法吧?

通过观望Dex Code和Native Code,能够推论,在dex2oat生成AOT机器码时是有做一些反省和优化的,由于dex2oat编写翻译机器码时肯定了四个措施同属三个类,所以机器码中就不设有权限检查有关代码。

2、同包名下

而是实际不是怀有办法都宛如类内部平昔访问那样顺遂的。

补丁类正在访问同包名下的类时会报出国访问谈极度。

切切实实的校验逻辑是在设想机代码的Class::IsInSamePackage中,关键点在于比较五个Class的所属的ClassLoader。

进而这里还亟需将新类的ClassLoader设置为与原本近似。

3、反射调用非静态方法

当一个非静态方法被热替换后,再反射调用那么些方法,会抛出卓殊。

在反射Invoke二个艺术时,在底部会掉啊用到InvokeMethod -> VerifyObejctIsClass函数做表达。

鉴于热替换方案锁替换的非静态方法,在反射调用者,由于VerifyObjectIsCLass时,旧类和新类不协作,就能够形成验证不经过。

一经是静态方法,会有平等的主题素材啊?

本来没有,静态方法会在类的等级直接开展调用的,无需经受对象实例作为参数,不会有那上面包车型地铁检查。

故此,对于这种反射调用非静态方法的标题,Sophix会采纳另一种冷运转机制对付,最终会有介绍。

本文由ca88发布,转载请注明来源

关键词: Sophix Android Andr 代码 方案