Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

android热修复技术原理-sophix #1

Open
xiewenfeng opened this issue Jul 11, 2017 · 1 comment
Open

android热修复技术原理-sophix #1

xiewenfeng opened this issue Jul 11, 2017 · 1 comment

Comments

@xiewenfeng
Copy link
Owner

No description provided.

@xiewenfeng
Copy link
Owner Author

xiewenfeng commented Jul 11, 2017

sophix热修复技术:
Android修复一般分三步,代码热修复、资源热修复、so包热修复,阿里经过研究业界的各种修复技术,最终出版了sophix技术,代码修复结合了热修复和冷启动两种修复方式,资源热修复采用了重命名资源的packageId,so库采用重命名,让so库提前加载,整体性能比现在业界的各种修复方案效果都好,具体使用手册可参考[管理后台使用手册]https://help.aliyun.com/document_detail/51434.html?spm=5176.doc53287.6.552.vZxNDm
下面是本人结合《深入探索Android热修复技术原理》一书的学习笔记,没有原理性的东西,主要是研究它的技术点。

1. 代码热修复

1.1 传统两大方案:

(1) 底层替换方案:在已加载的类中直接替换掉原有方法,在原来类基础上修改;
优点:时效性好,加载轻快,立即见效;
缺点:无法实现对原有类进行方法和字段增减,这样将破坏原有类结构;兼容性差;
市场上已经的修复方案:
Dexposed, andfix或其他Hook方案,直接依赖修改虚拟机实现的具体字段,如改Dalvi方法的jni函数指针、类或方法访问权限等,它们都是基于Android开源代码改的,如果厂商把ArtMethod类结构修改,则无法兼容。

(2) 类加载方案:在app重启后让Classloader去加载新的类。
优点:修复范围广,限制少;
缺点:时效性差,需要冷启动才能见效;
一些修复方案:

  1. QQ空间:采用插桩方式解决Dalvik下unexpected dex problem问题,单独放一个帮助类在独立的dex中让其他类调用,防止类被打上CLASS_ISPREVERIFIED标志。加载补丁dex得到dexFile对象,构建一个Element对象插入到dexElements数组的最前面。
    优点:没有合成包,产物小,灵活;
    缺点:侵入打包流程,为了hack添加一些无用信息,实现起来不优雅;Dalvik下影响类加载性能,Art下类地址写死,导致必包含父类/引用,导致补丁包大。
    总结:插桩导致所有类都非preverify,这导致verify和optimize操作都会在加载类时触发,类加载有一定的性能损耗。
  2. QFix:需要获取底层虚拟机的函数,不够稳定可靠,且无法新增public函数;
  3. Tinker:提供dex差量包,将patch.dex与应用的class.dex合并成一个完整的dex,得到dexFile对象作为参数,构建一个Element对象,再整体替换旧的dexElements数组。
    优点:补丁包小,dex merge成完整的dex,Dalvik不影响加载性能,Art下也不存在必须包含父类/引用类的情况;
    缺点:dex合并内存消耗在vm heap上,容易OOM,导致dex合并失败。
    总结:Tinker方案:完整的全量dex加载,从dex折方法和指令维度进行全量合成,比较粒度过细,实现复杂,性能消耗严重。

1.2 sophix采用的方案

sophix将两种方案结合,在此两种方案基础上进行改进的方案:
在生成补丁联合体,补丁工具会根据实据代码变动情况自动选反地,小修改,在底层修改方案限制范围内的,直接采用底层替换修复,做到代码修改即时生效;
超过底层替换限制的代码修改,使用类加载替换,冷启动修复;
(1) sophix底层修复改进方案:忽略底层ArtMethod结构差异,对所有Android版本都不需要区分,统一以memcpy实现,代码量减少,且只要保证ArtMethod数组仍是线性结构,就能适用Android各大版本。
(2) sophix类替换方案改进:基线包dex里去掉补丁包中的class,原先需要发生变更的旧class被消除,基线包dex里只包含不变的class.而修改的class要用到补丁中的新class时会自动找到补西dex。对于从原dex中去掉补丁包中class,只需要在解析这个dex时找不到class定义即可,只需移除定义的入口,对于class的具体内容不进行删除,这样可最大可能减少offset的修改。
sophix在Art下实现冷启动
Art下支持加载压缩文件中包含的多个dex,优先加载主dex(classes.dex),后续加载其他dex。所以解决办法是将补丁类放到classes.dex中,原apk中的dex依次命名为classes(2, 3, 4,...).dex,再一起打包成一个压缩文件,所续出现在其他dex类的补丁类是不会被重复加载。DexFile.loadDex得到DexFile对象,最后把该DexFile对象整体替换旧的dexElements数组就可以。

2. 资源热修复

Instant Run资源热修复分两步:

  1. 构造一个新的Assetmanager,通过反射调用addAssetPath, 把这个完整的资源包加入到AssetManager中,得到一个含有所有新资源的AssetManager;
  2. 找到所有之前引用的原有AssetManager的地方,通过反射,把引用处替换为含有新资源的AssetManager;
    sophix的方案:构造一个package id为0x66的资源包,这个包中只包含改变的资源项,直接在原AssetManager中addAssetPath这个包就可以了。

3. so库修复

把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能达到加载so库时是补丁so库,而非原来so库的目录。

4. 其他知识

4.1 android虚拟机加载dex的过程

加载一个dex文件到本地内存时,若不存在odex文件,会先执行dexopt,最后调用verifyAndOptimizeClass执行verify/optimize操作。
(1) dvmVerfiClass:类校验,防止类被篡改校验类的合法性。会对类的每个方法进行校验,如果类的所有方法中直接引用到的类和当前类在同一个dex中,dvmVerifyClass就返回true,此dex中的类就会被打上CLASS_ISPREVERIFIED标志。
(2) dvmOptimizeClass: 类优化,把部分指令优化成虚拟机内部指令,如方法调用指令:invoke- *指令变成了invoke- * -quick,加快方法执行速率,类被打上CLASS_ISOPTIMIZED标志。

4.2 类加载过程中:会依次调用resolve -> link -> init

(1) dvmResolveClass: 类解析,确认方法能正常被调用,如果类被打上了CLASS_ISPREVERIFIED标志,而被调用的类却和它不在同一个dex中,会抛出dvmThrowIllegalAccessError。为了解决此问题,QQ空间采用将一个单独帮助类放到一个单独的dex中,原dex所有类的构造函数都引用这个类(侵入dex打包流程,利用.class字节码修改技术,在所有.class文件构造函数中引用这个帮助类),这就是所谓的插桩实现。使得dexopt过程中dvmVerifyClass类校验近回false,原dex中所有类都没有CLASS_ISPREVERIFIED标志,解决运行时这个异常。
(2) dvmLinkClass: 父类/实现接口权限检查。
(3) dvmInitClass: 类解析完毕后对类进行初始化操作,完成父类和当前类、static变量的初始化等操作;如果类没有被打上CLASS_ISPEVERIFIED标志,会再次进行dvmVerfiClass类校验,若没有打上CLASS_ISOPTIMIZED标志,会再次进行类Optimize操作。正常情况下verify和optimize都仅是在apk第一次安装执行dexopt,而由于为了解决调用补丁包中方法报错,QQ空间采用的插桩方法,会使类的加载效率带来比较大的影响后果。

4.3 内部类编译

内部类编译:内部类会在编译期被编译为跟外部类一样的顶级类,非静态内部类,编译期间自动合成this$0域表示外部类的引用,非静态内部类会持有外部类的引用,静态内部类不持有外部类的引用。外部类为访问内部类私有域/方法,编译期间会为内部类生成access&**相关方法。
匿名内部类编译:匿名内部类就是没有名字的类,编译后类名一般是: 外部类&number,后面的number是编译期根据匿名内部类在外部类中出现的先后关系,依次累加命名的。

4.4 方法编译

如果混淆配置文件中添加上了-dontoptimize这项就不会做方法裁剪和内联,但是没有加则会进行。
如果应用了混淆,可能导致方法的内联和裁剪,有可能导致method的新增/减少。
方法内联发生的情况:

  1. 方法没有被其他任何地方用到,该方法会被内联到;
  2. 方法足够简单,如一个方法实现只有一行,该方法会被内联到,任何调用该方法的地方都会被该方法实现替换掉;
  3. 方法只被一个地方引用到,这个地方会被方法的实现替换掉;
    方法裁剪:如果方法中参数未被使用,则可能在编译期间将该参数忽略掉,变成无参函数

4.5 有关混淆

input.jars --shrink--> shrunk code --optimize--> optimized code --obfuscate--> obfucated code --preverify--> output.jars

  1. shrink:查找工程中类/类成员是否被使用,若未被使用,将其移除。
  2. optimize: 进一步优化代码,不是入口点的类/方法可以被设置为private/static/final,无用的参数可能被移除,一些方法可能被内联。
    可通过设置-dontoptimize不进行方法内联和裁剪
  3. obfuscate:针对不是入口点的类和类成员进行重命名,保持入口点类原始名称,以保证入口点类通过原名可访问到。
  4. preverify:针对.class文件预校验,在.class文件中加入StackMa/StackMapTable信息,这样Hotspot VM在类加载时执行类校验阶段会省去一些步骤,因此类加载更快。
    android虚拟机有自己一套代码校验逻辑(dvmVerifyClass),所以一般不校验,否则会拖慢打包速度,可通过-dontpreverify设置不校验。

4.6 Java中的泛型

泛型完全在编译器中实现,由编译器执行类型检查和类型推断,再生成普通的非泛型字节码,也称为擦除技术。编译器使用泛型类型信息保证类型安全,再在生成字节码前将其清除。

4.7lambda表达式编译规则:

lambda为java添加缺失的函数式编程特点。
函数式接口表示具有唯一的一个抽象方法的接口,如Runnable, Comparator
lambda表达式跟匿名内部类比:
关键字this:匿名内部类this指向匿名类,而lambda表达式的this指向包围lambda表达式的类;
编译方式:Java编译器将lambda表达式编译成类的私有方法,使用了Java7的invokedynamic字节码指令来动态绑定这个方法。匿名内部类则被编译成外部类&number的新类;
invokedynamic:支持动态语言的指令,允许方法调用可以在运行时指定类和方法,不必在编译的时候确定。字节码中每条invokedynamic指令出现的位置称为一个动态调用点,invokedynamic指令后面会跟一个指向常量池的调用点限定符,这个符会被解析为一个动态调用点。
为了使用java8中的lambda表达式特性,需要使用新的Jack工具链,新的Jack工具链直接将.java->.jack->.dex,而旧版javac工具链是.java->.class->.dex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant