热修复之比较

 

三.热修复框架

概述:

热修复其实很简单,通俗理解就找到有bug的apk和无bug的apk的差异生成一个.apatch(按照AndFix使用)结尾的文件,通过预先固定的通道从网上下载无bug的代码替换有bug的代码,从而实现bug的修复,最关键的是用户体验好,如果按照正常的流程操作的话需要开发人员修复完bug后打包经过测试人员测试后,上传到多个应用市场;通过热修复的方法就省去了很大的人力物力成本。

  • 底层替换方案:阿里的AndFix、HotFix
  • 类加载方案:QQ空间补丁技术、微信的Tinker方案、饿了么的Amigo
  • 二者结合:Sophix

目前最主要有三种方案:

Native Hook 进行底层替换---java虚拟机的热修复(hotfix)

 基于类加载与 Dex 分包方案,进行 Dex插桩/替换(基于Classloader的热修复(tinker)) ;
Install Run 进行类的注入 ;由于国内手机厂商定制系统的多样,Dex 插桩/替换是我认为最适合的方案。
当然还有其他的,目前阿里的Sophix据说是非侵入式。

1.1 . 基于虚拟机的热修复:

原理很简单,需要在c层去处理。  在底层会有一个叫ArtMethod的对象保存方法的描述 ,要做的就是替换这个对象,具体实现后续补充吧。

这个的原理需要了解一点java虚拟机的类加载机制。java虚拟机内存模型中有方法区,堆区,栈区。

我们写好的类的class字节码就在方法区中, 方法区中为每个类生成一个方法表,hotfix会用到这张表;new出来的对象就保存在

堆区中; 对象调用方法会从方法区中的方法表找到方法 压到方法栈中成为占帧。

那么修复用的原理就是把方法区中的方法字节码替换成已经修改好的,然后去执行。

2、热修复:Andfix为例子

热修复的原理:
我们知道Java虚拟机 —— JVM 是加载类的class文件的,而Android虚拟机——Dalvik/ART VM 是加载类的dex文件,
而他们加载类的时候都需要ClassLoader,ClassLoader有一个子类BaseDexClassLoader,而BaseDexClassLoader下有一个
数组——DexPathList,是用来存放dex文件,当BaseDexClassLoader通过调用findClass方法时,实际上就是遍历数组,
找到相应的dex文件,找到,则直接将它return。而热修复的解决方法就是将新的dex添加到该集合中,并且是在旧的dex的前面,
所以就会优先被取出来并且return返回。

Dex插桩原理:
ClassLoader 是通过调用 findClass 方法,在 pathList 对象中的 dexElements[] 中遍历dex文件寻找相关的类。由于靠前的dex会优先被系统调用,所以就有了插桩的概念。将修复好的 dex 插入到 dexElements[] 的最前方,这样系统就会调用修复好的插入类而不是靠后的 bug 类。

上图中,patch.dex 是插入的 dex ,classes2.dex 是原有的 bug dex。ClassLoader在遍历时优先获取了 patch.dex 中的 D.class ,所以 classes2.dex 中的 D.class 就不会被调用,这样就完成了对 D.class 的替换,修复了bug。

本文简单介绍了代码修复的技术原理,下篇文章将从系统源码入手,结合我自己封装的代码修复开源框架Fettler,详细解读代码修复的每一个过程。  

 热修复的原理:

Android的类加载器有两种:  PathClassLoader和DexClassLoader,两者的父类是BaseDexClassLoader,  BaseDexClassLoader的父类是ClassLoader

其中PathDexLoader用来加载系统类和应用类;

DexClassLoader用来加载一些jar、apk、dex文件,其实jar和apk文件实际上加载的都是dex文件。

热修复原理:

ClassLoader 会遍历一个由dex文件组成的数组,然后加载其中的dex文件,我们会把正确的dex(修复过的类所在的dex)文件  插入数组的前面, 当加载器 加载到好的类文件时候就不会加载有bug的类了,就实现了热修复

1,基于ClassLoad的修复实现

原理:在android中有两个常用ClassLoader,PathClassLoader加载已安装apk中class,DexClassLoader加载未安装apk或者aar中class.两个有一个共同的父类,BaseDexClassLoader,在BaseDexClassLoader->DexPathList->Element[] dexElements

中存储着apk或者aar中所有dex的集合。class加载类是从头遍历这个集合找到class就返回不会再往下找,这样我们就可以把修改好的dex查在数组的前边,让类加载器选择我们修改好的class(不知道算不算是一个bug)。

二.热修复框架的对比?

热修复框架的种类繁多,按照公司团队划分主要有以下几种:

阿里系 AndFix、Dexposed、阿里百川、Sophix
腾讯系 微信的Tinker、QQ空间的超级补丁、手机QQ的QFix
知名公司 美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso
其他 RocooFix、Nuwa、AnoleFix

虽然热修复框架很多,但热修复框架的核心技术主要有三类,分别是代码修复、资源修复和动态链接库修复。

虽然很多热修复框架采用了类加载方案,但具体的实现细节和步骤还是有一些区别的。

比如QQ空间的超级补丁和Nuwa是按照上面说得,将补丁包放在Element数组的第一个元素得到优先加载。

微信Tinker将新旧apk做了diff,得到patch.dex,然后将patch.dex与手机中apk的classes.dex做合并,生成新的classes.dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素。

饿了么的Amigo则是将补丁包中每个dex 对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element数组。

采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、QQ空间的超级补丁、手机QQ的QFix、饿了么的Amigo和Nuwa等等。

2.1 底层替换方案
与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改原有类,由于是在原有类进行修改限制会比较多,不能够增减原有类的方法和字段,如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况。

底层替换方案和反射的原理有些关联,就拿方法替换来说,方法反射我们可以调用java.lang.Class.getDeclaredMethod,

假设我们要反射Key的show方法,会调用如下所示。

Key.class.getDeclaredMethod("show").invoke(Key.class.newInstance());
Android 8.0的invoke方法。
libcore/ojluni/src/main/java/java/lang/reflect/Method.java

@FastNative
public native Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException; 

invoke方法是个native方法,对应Jni层的代码为:
art/runtime/native/java_lang_reflect_Method.cc

static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
jobject javaArgs) {
ScopedFastNativeObjectAccess soa(env);
return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
 
Method_invoke函数中又调用了InvokeMethod函数:
art/runtime/reflection.cc

jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
jobject javaReceiver, jobject javaArgs, size_t num_frames) {

...
ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
const bool accessible = executable->IsAccessible();
ArtMethod* m = executable->GetArtMethod();//1
...
}
注释1处获取传入的javaMethod(Key的show方法) 在ART虚拟机中对应的一个ArtMethod指针,

ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等等,ArtMethod结构如下所示。

art/runtime/art_method.h

class ArtMethod FINAL {
...
protected:
GcRoot<mirror::Class> declaring_class_;
std::atomic<std::uint32_t> access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;
uint16_t hotness_count_;
struct PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;//1
void* data_;
void* entry_point_from_quick_compiled_code_;//2
} ptr_sized_fields_;
}

替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,这就是底层替换方案。

AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败;

Sophix采用的是替换整个ArtMethod结构体,这样不会存在兼容问题。
底层替换方案直接替换了方法,可以立即生效不需要重启。采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。

注释1处获取传入的javaMethod(Key的show方法) 在ART虚拟机中对应的一个ArtMethod指针,

ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等等,ArtMethod结构如下所示。

ArtMethod结构中比较重要的字段是注释1处的dex_cache_resolved_methods_和注释2处的entry_point_from_quick_compiled_code_,它们是方法的执行入口,

当我们调用某一个方法时(比如Key的show方法),就会取得show方法的执行入口,通过执行入口就可以跳过去执行show方法。

3.3 Instant Run方案
除了资源修复,代码修复同样也可以借鉴Instant Run的原理, 可以说Instant Run的出现推动了热修复框架的发展。
Instant Run在第一次构建apk时,使用ASM在每一个方法中注入了类似如下的代码:

IncrementalChange localIncrementalChange = $change;//1
if (localIncrementalChange != null) {//2
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
paramBundle });
return;
}
 
其中注释1处是一个成员变量localIncrementalChange ,它的值为$change,$change实现了IncrementalChange这个抽象接口。

当我们点击InstantRun时,如果方法没有变化则$change为null,就调用return,不做任何处理。如果方法有变化,就生成替换类,这里我们假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivity$override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的$change设置为MainActivity$override,因此满足了注释2的条件,会执行MainActivity$override的access$dispatch方法,accessdispatch方法中会根据参数&quot;onCreate.(Landroid/os/Bundle;)V&quot;执行‘MainActivitydispatch方法中会根据参数&quot;onCreate.(Landroid/os/Bundle;)V&quot;执行`MainActivitydispatch方法中会根据参数"onCreate.(Landroid/os/Bundle;)V"执行‘MainActivityoverride`的onCreate方法,从而实现了onCreate方法的修改。
借鉴Instant Run的原理的热修复框架有Robust和Aceso。

代码修复:
代码修复主要有三个方案,分别是底层替换方案、类加载方案和Instant Run方案。

3.1 类加载方案
类加载方案基于Dex分包方案,什么是Dex分包方案呢?这个得先从65536限制和LinearAlloc限制说起。
65536限制
随着应用功能越来越复杂,代码量不断地增大,引入的库也越来越多,可能会在编译时提示如下异常:

com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
1
这说明应用中引用的方法数超过了最大数65536个。产生这一问题的原因就是系统的65536限制,65536限制的主要原因是DVM Bytecode的限制,

DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用 65535个方法。
LinearAlloc限制
在安装时可能会提示INSTALL_FAILED_DEXOPT。产生的原因就是LinearAlloc限制,DVM中的LinearAlloc是一个固定的缓存区,当方法数过多超出了缓存区的大小时会报错。

为了解决65536限制和LinearAlloc限制,从而产生了Dex分包方案。

Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。

当应用启动时先加载主Dex,等到应用启动后再动态的加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制。

Dex分包方案主要有两种,分别是Google官方方案、Dex自动拆包和动态加载方案。因为Dex分包方案不是本章的重点。
————————————————
 在Android解析ClassLoader(二)Android中的ClassLoader中讲到了ClassLoader的加载过程,其中一个环节就是调用DexPathList的findClass的方法,如下所示。

Element内部封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element。
多个Element组成了有序的Element数组dexElements。当要查找类时,会在注释1处遍历Element数组dexElements(相当于遍历dex文件数组),注释2处调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。如果在Element中(dex文件)找到了该类就返回,如果没有找到就接着在下一个Element中进行查找。
根据上面的查找流程,我们将有bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换之前存在bug的Key.class,排在数组后面的dex文件中的存在bug的Key.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案,如下图所示。

类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启呢?这是因为类是无法被卸载的,因此要想重新加载新的类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。

三、阿里百川HotFix
阿里百川推出的热修复HotFix服务,相对于QQ空间超级补丁技术和微信Tinker来说,定位于紧急BUG修复的场景下,能够最及时的修复BUG,下拉补丁立即生效无需等待。
图片描述
1、AndFix实现原理
AndFix不同于QQ空间超级补丁技术和微信Tinker通过增加或替换整个DEX的方案,提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。
原理图如下:
图片描述
2、AndFix实现过程

对于实现方法的替换,需要在Native层操作,经过三个步骤:
图片描述
接下来以Dalvik设备为例,来分析具体的实现过程:

AndFix对ART设备同样支持,具体的过程与Dalvik相似,这里不再赘述。

从技术原理,不难看出阿里百川HotFix的几个特点:

优势:

  1. BUG修复的即时性

  2. 补丁包同样采用差量技术,生成的PATCH体积小

  3. 对应用无侵入,几乎无性能损耗
    不足:

  4. 不支持新增字段,以及修改<init>方法,也不支持对资源的替换。

  5. 由于厂商的自定义ROM,对少数机型暂不支持。

二、微信Tinker
微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,

区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,

然后整体替换掉旧的class.dex文件,以达到修复的目的。
图片描述
我们来逆向微信的APK看一下具体的实现:

从代码中可以看到, 通过反射 操作得到PathClassLoader的DexPatchList, 反射调用patchlist的makeDexElements()方法,

把本地的dex文件直接替换到Element[]数组中去,达到修复的目的。

对于如何进行patch.dex与classes.dex的合并操作,这里微信开启了一个新的进程,开启新进程的服务TinkerPatchService 进行合并。
图片描述
整体的流程如下:
图片描述
从流程图来看,同样可以很明显的找到这种方式的特点:
       优势:

  1. 合成整包,不用在构造函数插入代码,防止verify,verify和opt在编译期间就已经完成,不会在运行期间进行。

  2. 性能提高。兼容性和稳定性比较高。

  3. 开发者透明,不需要对包进行额外处理。

         不足:

  1. 与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。

  2. 需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。

  3. 合并时占用额外磁盘空间,对于多DEX的应用来说,如果修改了多个DEX文件,就需要下发多个patch.dex与对应的classes.dex进行合并操作时这种情况会更严重,因此合并过程的失败率也会更高。

 

一、QQ空间超级补丁技术
超级补丁技术基于DEX分包方案,使用了多DEX加载的原理,大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。
图片描述
当patch.dex中包含Test.class时就会优先加载,在后续的DEX中遇到Test.class的话就会直接返回而不去加载,这样就达到了修复的目的。

但是有一个问题是,当两个调用关系的类不在同一个DEX时,就会产生异常报错。我们知道,在APK安装时,虚拟机需要将classes.dex优化成odex文件,然后才会执行。在这个过程中,会进行类的verify操作,如果调用关系的类都在同一个DEX中的话就会被打上CLASS_ISPREVERIFIED的标志,然后才会写入odex文件。

所以,为了可以正常地进行打补丁修复,必须避免类被打上CLASS_ISPREVERIFIED标志,具体的做法就是单独放一个类在另外DEX中,让其他类调用。

修复的步骤为:

  1. 可以看出是通过获取到当前应用的Classloader,即为BaseDexClassloader

  2. 通过反射获取到他的DexPathList属性对象pathList

  3. 通过反射调用pathList的dexElements方法把patch.dex转化为Element[]

  4. 两个Element[]进行合并,把patch.dex放到最前面去

  5. 加载Element[],达到修复目的

整体的流程图如下:
图片描述
从流程图来看,可以很明显的找到这种方式的特点:
优势:

  1. 没有合成整包(和微信Tinker比起来),产物比较小,比较灵活

  2. 可以实现类替换,兼容性高。(某些三星手机不起作用)
    不足:

  3. 不支持即时生效,必须通过重启才能生效。

  4. 为了实现修复这个过程,必须在应用中加入两个dex!dalvikhack.dex中只有一个类,对性能影响不大,但是对于patch.dex来说,修复的类到了一定数量,就需要花不少的时间加载。对手淘这种航母级应用来说,启动耗时增加2s以上是不能够接受的事。

  5. 在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。

一. 热修复介绍
1.开发流程

当项目出现紧急bug时,传统的开发流程是发布新版本,引导用户覆盖安装。抛开平台审核上线的时间不说,一天重复下载安装至少两次的用户体验是很差的。

而热修复的出现完美解决了这个问题,用户在收到服务器推送过来的修复包后,在项目运行时进行修复。

整个过程是在用户无感知状态下完成,也无需下载相对来说较大的安装包,代价小。

总结为两个优点:

无需重新发版,修复效率高; 用户无感知,代价小

2.都能修复什么

资源修复
代码修复
so库修复

一、代码修复

热修复框架分析

  • 底层替换方案:阿里的AndFix、HotFix
  • 类加载方案:QQ空间补丁技术、微信的Tinker方案、饿了么的Amigo
  • 二者结合:Sophix

1、类加载方案

(1)Dex分包原理

   单个Dex文件里面方法数不能超过65536个方法。

(1)原因:
因为android会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的, short占两个字节(保存-2的15次方到2的15次方-1,即-32768~32767),最大保存的数量就是65536。

(2)解决方案:

  • 精简方法数量,删除没用到的类、方法、第三方库。
  • 使用ProGuard去掉一些未使用的代码
  • 对部分模块采用本地插件化的方式。
  • 分割Dex

Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。

当应用启动时先加载主Dex,等到应用启动后再动态的加载次Dex。

(2)类加载修复方案

如果Key.Class文件中存在异常,将该Class文件修复后,将其打入Patch.dex的补丁包
(1) 方案一:
通过反射获取到PathClassLoader中的DexPathList,然后再拿到 DexPathList中的Element数组,将Patch.dex放在Element数组dexElements的第一个元素,最后将数组进行合并后并重新设置回去。在进行类加载的时候,由于ClassLoader的双亲委托机制,该类只被加载一次,也就是说Patch.dex中的Key.Class会被加载。

 (2)方案二:
提供dex差量包patch.dex,将patch.dex与应用的classes.dex合并成一个完整的dex,完整dex加载后得到dexFile对象,作为参数构建一个Element对象,然后整体替换掉旧的dex-Elements数组。(Tinker) 

(3)类加载方案的限制

方案一:

  • 由于类是无法进行卸载,所以类如果需要重新加载,则需要重启App,所以类加载修复方案不是即时生效的。
  • 在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包大,耗时严重。

方案二:

  • 下次启动修复
  • dex合并内存消耗可能导致OOM,最终dex合并失败

2、底层替换方案

(1)基本方案

 主要是在Native层替换原有方法, ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等。

替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,就是底层替换方案。  由于直接替换了方法,可以立即生效不需要重启。

(2)优缺点

(1)缺点

  • 不能够增减原有类的方法和字段,如果我们增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法。
  • 平台兼容性问题,如果厂商对ArtMethod结构体进行了修改,替换机制就有问题。

(2)优点

  • Bug修复的即时性
  • 生成的PATCH体积小,性能影响低

二、资源修复

1、资源包替换(Sophix)

默认由Android SDK编译出来的apk,其资源包的package id为0x7f。framework-res.jar的资源package id为0x01

  • 构造一个package id为0x66的资源包(非0x7f和0x01),只包含已经改变的资源项。
  • 由于不与已经加载的Ox7f冲突,所以可以通过原有的AssetManager的addAssetPath加载这个包。

2、Instant Run

  • 反射构建新的AssetManager,并反射调用addAssertPath加载sdcard中的新资源包,这样就得到一个含有所有新资源的AssetManager
  • 将原来引用到AssetManager的地方,通过反射把引用处 替换为新的AssetManager

核心代码:runtime/MonkeyPatcher.java

  1.  
    #MonkeyPatcher
  2.  
    public static void monkeyPatchExistingResources(@Nullable Context context,
  3.  
    @Nullable String externalResourceFile,
  4.  
    @Nullable Collection<Activity> activities) {
  5.  
    ......
  6.  
    try {
  7.  
    // Create a new AssetManager instance and point it to the resources installed under
  8.  
    // (1)通过反射创建了一个newAssetManager,调用addAssetPath添加了sdcard上的资源包
  9.  
    AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
  10.  
    Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
  11.  
    mAddAssetPath.setAccessible(true);
  12.  
    if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
  13.  
    throw new IllegalStateException("Could not create new AssetManager");
  14.  
    }
  15.  
    // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
  16.  
    // in L, so we do it unconditionally.
  17.  
    Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
  18.  
    mEnsureStringBlocks.setAccessible(true);
  19.  
    mEnsureStringBlocks.invoke(newAssetManager);
  20.  
    if (activities != null) {
  21.  
    //(2)反射获取Activity中AssetManager的引用,替换成新创建的newAssetManager
  22.  
    for (Activity activity : activities) {
  23.  
    Resources resources = activity.getResources();
  24.  
    try {
  25.  
    Field mAssets = Resources.class.getDeclaredField("mAssets");
  26.  
    mAssets.setAccessible(true);
  27.  
    mAssets.set(resources, newAssetManager);
  28.  
    } catch (Throwable ignore) {
  29.  
    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
  30.  
    mResourcesImpl.setAccessible(true);
  31.  
    Object resourceImpl = mResourcesImpl.get(resources);
  32.  
    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
  33.  
    implAssets.setAccessible(true);
  34.  
    implAssets.set(resourceImpl, newAssetManager);
  35.  
    }
  36.  
    Resources.Theme theme = activity.getTheme();
  37.  
    try {
  38.  
    try {
  39.  
    Field ma = Resources.Theme.class.getDeclaredField("mAssets");
  40.  
    ma.setAccessible(true);
  41.  
    ma.set(theme, newAssetManager);
  42.  
    } catch (NoSuchFieldException ignore) {
  43.  
    Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
  44.  
    themeField.setAccessible(true);
  45.  
    Object impl = themeField.get(theme);
  46.  
    Field ma = impl.getClass().getDeclaredField("mAssets");
  47.  
    ma.setAccessible(true);
  48.  
    ma.set(impl, newAssetManager);
  49.  
    }
  50.  
    ......
  51.  
    }
  52.  
    //(3)遍历Resource弱引用的集合,将AssetManager替换成newAssetManager
  53.  
    for (WeakReference<Resources> wr : references) {
  54.  
    Resources resources = wr.get();
  55.  
    if (resources != null) {
  56.  
    // Set the AssetManager of the Resources instance to our brand new one
  57.  
    try {
  58.  
    Field mAssets = Resources.class.getDeclaredField("mAssets");
  59.  
    mAssets.setAccessible(true);
  60.  
    mAssets.set(resources, newAssetManager);
  61.  
    } catch (Throwable ignore) {
  62.  
    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
  63.  
    mResourcesImpl.setAccessible(true);
  64.  
    Object resourceImpl = mResourcesImpl.get(resources);
  65.  
    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
  66.  
    implAssets.setAccessible(true);
  67.  
    implAssets.set(resourceImpl, newAssetManager);
  68.  
    }
  69.  
    resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
  70.  
    }
  71.  
    }
  72.  
    } catch (Throwable e) {
  73.  
    throw new IllegalStateException(e);
  74.  
    }
  75.  
    }
  76.  

三、SO库修复

本质是对native方法的修复和替换

2、SO修复方案

(1)接口替换

    提供方法替代System.loadLibrary方法:

  • 如果存在补丁so,则加载补丁so库,不去加载apk安装目录下的so库
  • 如果不存在补丁so,调用System.loadLibrary去加载安装apk目录下的so库

(2)反射注入

     因为加载so库会遍历nativeLibraryDirectories

  • 通过反射将补丁so库的路径插入到nativeLibraryDirectories数组的最前面
  • 遍历nativeLibraryDirectories时,就会将补丁so库进行返回并加载,从而达到修复目的

1、so库加载

(1)通过以下方法加载so库.

类似于类加载的findClass方法,在数组中每一个元素对应一个so库,最终返回了so的路径。

如果将so补丁添加到数组的最前面,在调用方法加载so库时,会先将补丁so的路径返回。

  1.  
    #System
  2.  
    public static void loadLibrary(String libname) {
  3.  
    Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
  4.  
    }
  5.  
    参数为so库名称,位于apk的lib目录下
  6.  
     
  7.  
    public static void load(String filename) {
  8.  
    Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
  9.  
    }
  10.  
    加载外部自定义so库文件,参数为so库在磁盘中的完整路径
  11.  
  12.  
    private static native String nativeLoad(String filename, ClassLoader loader, String librarySearchPath);
  13.  

最终都是调用了native方法nativeLoad,参数fileName为so在磁盘中的完整路径名

(2)遍历nativeLibraryDirectories目录

  1.  
    #DexPathList
  2.  
    public String findLibrary(String libraryName) {
  3.  
    String fileName = System.mapLibraryName(libraryName);
  4.  
    for (File directory : nativeLibraryDirectories) {
  5.  
    File file = new File(directory, fileName);
  6.  
    if (file.exists() && file.isFile() && file.canRead()) {
  7.  
    return file.getPath();
  8.  
    }
  9.  
    }
  10.  
    return null;
  11.  
    }

参考资料:

 具体实现:

      if (context == null) {
            return;
        }
        File filesDir = context.getDir("odex", Context.MODE_PRIVATE);
        File[]  listFiles=filesDir.listFiles();
        for (File file : listFiles) {
            if(file.getName().startsWith("classes")||file.getName().endsWith(".dex")){
                Log.i("INFO", "dexName:"+file.getName());
                loadedDex.add(file);
            }
        }
        String optimizeDir = filesDir.getAbsolutePath() + File.separator + "opt_dex";
        File fopt = new File(optimizeDir);
        if (!fopt.exists()) {
            fopt.mkdirs();
        }
        for (File dex : loadedDex) {
            DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

            try {
//                -----------------------系统的ClassLoader------------------------------------
                Class baseDexClazzLoader=Class.forName("dalvik.system.BaseDexClassLoader");
                Field  pathListFiled=baseDexClazzLoader.getDeclaredField("pathList");
                pathListFiled.setAccessible(true);
                Object pathListObject = pathListFiled.get(pathClassLoader);


                Class  systemPathClazz=pathListObject.getClass();
                Field  systemElementsField = systemPathClazz.getDeclaredField("dexElements");
                systemElementsField.setAccessible(true);
                Object systemElements=systemElementsField.get(pathListObject);

//                ------------------自己的ClassLoader--------------------------
                Class myDexClazzLoader=Class.forName("dalvik.system.BaseDexClassLoader");
                Field  myPathListFiled=myDexClazzLoader.getDeclaredField("pathList");
                myPathListFiled.setAccessible(true);
                Object myPathListObject =myPathListFiled.get(classLoader);


                Class  myPathClazz=myPathListObject.getClass();
                Field  myElementsField = myPathClazz.getDeclaredField("dexElements");
                myElementsField.setAccessible(true);
                Object myElements=myElementsField.get(myPathListObject);

//                ------------------------融合-----------------------------
                Class<?> sigleElementClazz = systemElements.getClass().getComponentType();
                int systemLength = Array.getLength(systemElements);
                int myLength = Array.getLength(myElements);
                int newSystenLength = systemLength + myLength;
//                生成一个新的 数组   类型为Element类型
                Object newElementsArray = Array.newInstance(sigleElementClazz, newSystenLength);
                for (int i = 0; i < newSystenLength; i++) {
                    if (i < myLength) {
                        Array.set(newElementsArray, i, Array.get(myElements, i));
                    }else {
                        Array.set(newElementsArray, i, Array.get(systemElements, i - myLength));
                    }
                }
//      ---------------------------融合完毕   将新数组  放到系统的PathLoad内部---------------------------------
                Field  elementsField=pathListObject.getClass().getDeclaredField("dexElements");;
                elementsField.setAccessible(true);

                elementsField.set(pathListObject,newElementsArray);

这样就完成了dex的插入替换。

比较以上两种方案比较喜欢第二种,应为第一种只能修改方法,  第二种可以整个类替换,切适配性很好(只要java不修改这个bug在哪都能用)。

posted on 2020-04-09 22:17  左手指月  阅读(723)  评论(0编辑  收藏  举报