本文主要描述完整可行的热修复技术设计方案,给出客户端与服务器端的相应改造, 并介绍核心实现原理、分析主流框架的优劣势.由于与插件化开发有一定重叠,因此也简单 介绍插件化应用的原理.
整体方案
背景
当应用发布之后,突然发现严重 bug 需要紧急修复.传统做法是修复后打包为新版 本 App, 再通过应用商店发布新版本,缺点是从修复到用户更新的过程非常缓慢,无法解 决紧急问题.
热修复技术通过线上发布补丁, 客户端自动获取补丁后立即加载,即时生效,可以非 常迅速的修复线上问题.
流程
本地、服务端、线上版本三方工作时序:
1) 通过各种途径获知 bug存在
2) 定位并在本地修复
3) 制作补丁包
4) 发布到服务器端
5) 线上版本收到推送
6) 下载后加载补丁
时序
触发
分为客户端主动触发与被动触发:
加载补丁
业务全景
全模块
客户端
1) 推送模块 PUSH
接收推送消息,分发补丁操作的指令.
2) 补丁管理模块 PatchManager
补丁持久化, 开机自检:管理存储空间,清除临时文件, 维护补丁版本与主程序版本的对应关系, 响应四种推送消息:增、删、改、查: 下 载补丁并加载,
删除指定补丁,
删除后重下载, 查询当前补丁数量和版本号.
3) 安全模块 Secure 校验补丁文件有效性,合法性, 校验文件指纹, 删除非法文件.
4) 热修复模块 Core 加载补丁.
5) 全局异常模块 UncaughtException 捕获全局异常,触发补丁自动下载.
6) 日志模块 Logger 管理日志,
主动上送运行异常.
7) 容错模块 ErrorHandler
校验叠加版本:当前主程序版本+补丁版本 回退机制:删除所有,删除一段时间, 重加载:触发热修复模块重加载.
服务端
1) 补丁管理平台 PatchPlatform 提供可视化操作界面: 手动上传、下载补丁文件, 发布补丁,触发推送模块, 统计修复成功率, 登录、用户权限.
2) 补丁管理模块 PatchManager 维护补丁版本与主程序版本对应关系, 配 置补丁属性:版本号,对应主程序版本, 增 删改查.
3) 推送模块 Push 推送补丁管理的相应指令, 推送日志操作的相应指令.
4) 日志模块 Logger 主动或被动获取日志, 跟踪设备使用情况.
热修复技术
实现原理
实现热修复功能的大方向有两种:NativeHook与替换 Dex.两种流派都各有优缺点, 目前并没有百分之百完美的方案.
Native Hook 方式
一般情况下,导致 crash 的代码都能最终定位到某一具体的函数, 例如最常见的空
指针异常:1
2
3
4
5com.tim.patcher E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.tim.patcher, PID: 20275
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.String.toString()' on a null object reference
at com.tim.patcher.MainActivity$1$override.onClick(MainActivity.java:25)
at android.app.ActivityThread.main(ActivityThread.java:5314) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:693)
线下修复的做法是通过上述栈信息找到 MainActivity类,修改第 25行的onClick方法, 而后打包发布.
Native 方式的出发点由此而来,如果能动态的将问题函数替换为需要的函数,使程 序在运行到这一步时不执行原函数而去执行新的函数,就能完成热修复了.示意图如下:
onClick()是问题函数,onClick_fix()是修复后的函数.执行 dispatch()后绕开 onClick(),转而执行 onClick_fix(),达到修复的目的.关键是用 onClick_fix()替代 onClick()这一步.
在 native层, java方法在 Dalvik/ART虚拟机中的实现都会由 C++的 ClassObject 和 Method对象表示,ClassObject记录类的属性,Method记录方法字节码的地址. 只要 在虚拟机执行到要修改的方法前通过其所在的 ClassObject 找到对应的 Method,将 Method的指针指向目标方法就能达到” 绕开” 的目的.
具体来说,在这个例子中先将onClick()标记为native方法,然后就可以在jni中获 取它的 ClassObject,通过 ClassObject获取 Method对象,再把 Method指向 java层的 onClick_fix()方法.
上述步骤只能替换特定的函数,因为最后一步 Method 指针指向了一个具体的函数. 站在” 造轮子” 的角度上看,实际的使用场景是事先并不能获知目标方法具体是什 么, 因此 Method 指向的目标一定要是可即时改变的.所以在此基础上再进一步,只需要 将指向具体的修复后函数改为指向一个 api 钩子,实现它的回调,就能适用于实际使用场 景, 替换任何待定的函数.
Dex 合并方式
Dex合并的方式来源于MultiDex还未发布时,为解决方法数限制2^16这一问题而 诞生的 Dex 分包方案.Dex 分包实现了动态加载,动态加载是插件化开发的基础,插件技术 后来引申出了这一类热修复方式.
一般来说,插件化是通过动态加载技术实现的,对插件框架稍加改造就可以实现热 修复,因此先简单介绍 Dex分包和插件技术.
Dex分包
谷歌为了优化安卓应用的运行性能,提出了数据结构和效率比 jar 更好的 dex 格式 .dex 类似于 jar,是一种储存字节码的归档文件.系统启动应用时会先对首次加载的 dex做优化,称为 DexOpt,它会生成一个 Optimised Dex文件.ODex会把应用中所有方 法的 id存储在链表中,但谷歌在这里犯了一个错误,用 short类型保存了这个链表的长度, 导致长度不能超过 short类型最大值 2^16.这就是方法数不能超过 65535的由来.Dex分 包通过将一个应用的 dex拆分成多个来规避这个问题.
插件技术
在 Java工程中可以很方便的把 class文件放到jar中直接实现动态加载,但安卓系统 并不支持这样做,因为 jvm执行 class文件而 dvm执行 dex文件.安卓虚拟机不能识别 java的字节码,要将 class转换成 dex文件才能识别,所以拆分的最小单位是 dex.
插件技术的关键在于如何动态加载多个 dex.加载插件的过程可以分解为加载代码 (.java)和加载资源(R resource),资源的加载和热修复无关,因此这里只介绍加载类的方 法.
Java中用 ClassLoader加载 jar,Android也有自己的 ClassLoader体系,通过这个 体系完成类的加载.如图所示:
DexClassLoader和 PathClassLoader是 BaseDexClassLoader的两种实现,他们都能 使被加载的类可以正常识别.两者的区别是 DexClassLoader 可以加载文件系统上的 jar,dex以及 apk,而 PathClassLoader需要提供固定的文件路径(比如
/data/app/com.bill99.ian.patcher/base-1.apk ),只能加载已安装到/data/app 下的应用. 在 Application 中系统分配了两种加载方式,安卓系统应用使用 DexClassLoader而非系统应用使用 PathClassLoader加载.
插件架构有自己重写 ClassLoader的方案也有直接使用 DexClassLoader让系统 帮助加载的方案.而使用 DexClassLoader的方案与合并 Dex完成热修复的方法非常接 近, 因此下面讨论通过 DexClassLoader加载类的方案.
DexClassLoader提供了 loadClass()方法进行类的加载 .loadClass()调用 BaseDexClassLoader中的 findClass(),findClass()根据类名查找到 dex中的 class文件 :1
2
3
4
5
6
7
8
9@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; }
源码中, findClass()实际上调用了 pathList 的 findClass(),pathList 是在 BaseDexClassLoader中创建的 DexPathList对象的实例,DexPathList中 findClass() 代码如下:1
2
3
4
5
6
7
8
9public 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; }
其实就是遍历 dexElements,调用每个元素的 dexFile对象的 loadClassBinaryName
方法.其中 dexElement在 DexPathList中创建:1
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
makeDexElements最终会调用 dvm对 dex文件进行优化和映射,完成类加载.
仅仅加载类文件是不够的,Activity,Service 等组件原来由 AMS 管理生命周期,单纯 的加载 Activity 等类后他们也没有了组件的特殊性,和普通的类没有任何区别.有一种解 决方法是通过在接口中定义各种生命周期方法,由插件的 Activity 实现接口,然后在主程 序中调用接口,人工的制造出生命周期.
Dex替换
插件动态加载的核心是通过ClassLoader加载多个不同的dex,每个dex中类的完整 路径不允许重复,否则会导致不同 dex之间的类加载错乱.而合并 Dex的热修复方式反其 道而行之,它加载一个命名空间和主程序重复的dex.
上图是DexClassLoader加载类的流程,关键是DexPathList中的dexElements数组. ClassLoader 会遍历数组中的 dex,从中找到与要加载的类名对应的 dex,加载完成后返 回 class文件.根据流程图能够得出热修复的关键线索:如果有两个 dex同时能够加载这个 类,也就是两个dex具有重复全路径的类,那么 ClassLoader会调用 dexElements数组中 排在前列的 dex去加载这个类,加载完成后立即返回,顺序排在其后的 dex不会被访问到.
对于动态加载框架来说,如果能把dex文件插入这个数组就能加载自己的类.而对于 热修复框架来说,把 dex 插入这个数组并保证其位置在数组最前就能将问题类替换成修复后的类.
由于系统没有提供 API 操作 DexPathList 或是 dexElements,需要通过反射从 BaseDexClassLoader 获取 dexPathList,再通过 DexPathList 获取 dexElements,反射 Element 的构造方法创建一个插件的 dexElement,再创建一个新的数组,长度为 dexEments 的长度加上插件 dexElement 的数量,把反射得到的 dexElements 和插件 dexElement对象一并插入数组,最后用新创建的数组替换原来的数组.过程如下图:
一般在 Application 的 onCreate()或 attachBaseContext()完成数组替换.至此已 经已经完成了热修复的主体.实际运行过程中还有其他问题,当一个类引用另一个类且他们不在同一个 dex中, 系统会抛出异常 Class resolved by unexpected DEX,原因是 从 dexElements查找到需要的 dex并返回 class后系统会对引用者与被引用者的 dex做 校验,当他们不相同时抛出异常.校验方式是如果 A类和 B类在同一个 dex中,那么 A类 就会被打上 CLASS_ISPREVERIFIED标记,A类被打上标记后不能再引用其他 dex中的类 , 否则报错.如果A类还引用了C类,而 C类在其他dex中,那么 A类将不会被打上标记,这 一点被利用来避免 unexpectedDex错误.大部分做法是在所有类的构造函数中都引用一 个空实现的类,将这个类单独打包为一个dex,在应用启动时先加载这个dex.把引用放在 构造函数中是因为构造函数不会导致方法数增加.在所有类中加入引用的工作量很大也 容易出错,可以用 AOP 的编译工具如 AspectJ 在编译源码时加入引用方法,也可以用 JavaAssist插入字节码.
小结
如上所述,热修复技术的两个流派分别是通过 Native Hook 替换问题函数和通过插 入 Dex替换问题类.
两个流派都有大规模实际应用,都被证明是稳定的,各有优点也都有不足之处,目前为 止尚未发现十全十美的方法,因此在选型时选择符合当前项目情况和最易使用、能够长 期维护的方案.
Native 的原理决定了它只能对函数做修改,不能替换资源如布局文件,图片.优点是任 何修复可以立即生效.
Dex 流派能够很好的支持类替换,lib 替换和资源替换,但它的致命缺点是不能即时生 效,加载补丁 dex后必须重启或等待下次开启应用才能生效.
主流框架
两个流派分别选取主流的、具有代表性的,并有实践案例的开源框架作对比.
Dexposed
Dexposed是最早开源的热修复SDK, 由阿里巴巴团队基于 Xposed框架实现,还能 用作 AOP编程、插桩、SDKHook,被应用在手机淘宝,天猫,MIUI等应用和系统.属于 Native 流派.
AndFix
AndFix 是支付宝的开源框架,应用于支付宝等 APP.AndFix 属于 Native 流派,在 Dexposed 基础上做了大量完善,支持 Android2.3-Android7.0 全版本,补丁编写没有 门槛,并提供补丁生成工具.缺点是不支持替换资源,不支持替换 lib,不能新增函数;如果补 丁过多会影响程序运行速度.
Nuwa
Nuwa属 于 Dex流派,是大众点评的一位工程师根据QQ空间团队分享的实现方式做 的. 优点是支持资源,lib 等替换,缺点在于不能实时生效,必须重启应用或下次开启生效.另 外由于所有类都会被打上预加载标记也会造成启动变慢.
Tinker
微信团队最近开源了 Tinker,我们直接从它的源码入手开始分析.
dev 分支上是最新的 Tinker1.6.1 版本,从类名可以知道 Tinker 处理了类的加载, 资源的加载以及 so 库的加载.我们的关注点在类加载上,根据经验判断,TinkerLoader 类是类加载模块的入口,因此从该类开始:1
2
3
4
5
6
7
8
9
10
11
12
13/**
* only main process can handle patch version change or incomplete
*/
@Override
public Intent tryLoad(TinkerApplication app) {
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
TinkerLoader.tryLoad()很明显就是加载 dex 的入口函数,这里微信统计了加载时 间,并进入 tryLoadPatchFilesInternal()方法.这个方法较长,主要是对新旧两个 dex 做 合并,这里截取其中关键的步骤:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//now we can load patch jar
if (isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);
if (isSystemOTA) {
// update fingerprint after load success
patchInfo.fingerPrint = Build.FINGERPRINT;
patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
// reset to false
oatModeChanged = false;
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
return;
}
// update oat dir
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
}
if (!loadTinkerJars) {
Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
return;
}
}
做 了 很 多 安 全 校 验 的 机 制 以 保 证 dex 可 用 后 , 调 用 TinkerDexLoader.
loadTinkerJars()方法.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {
if (loadDexList.isEmpty() && classNDexInfo.isEmpty()) {
Log.w(TAG, "there is no dex to load");
return true;
}
PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
if (classLoader != null) {
Log.i(TAG, "classloader: " + classLoader.toString());
} else {
Log.e(TAG, "classloader is null");
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
return false;
}
String dexPath = directory + "/" + DEX_PATH + "/";
loadTinkerJars()获取 PathClassLoader并读取dex与 dvm优化后的 odex地址,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// verify merge classN.apk
if (isVmArt && !classNDexInfo.isEmpty()) {
File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
long start = System.currentTimeMillis();
if (application.isTinkerLoadVerifyFlag()) {
for (ShareDexDiffPatchInfo info : classNDexInfo) {
if (!SharePatchFileUtil.verifyDexFileMd5(classNFile, info.rawName, info.destMd5InArt)) {
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
classNFile.getAbsolutePath());
return false;
}
}
}
Log.i(TAG, "verify dex file:" + classNFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
legalFiles.add(classNFile);
}
File optimizeDir = new File(directory + "/" + oatDir);
接着遍历 dexList,过滤 md5 不符校验不通过的,调用 SystemClassLoaderAdder 的 installDexs()方法.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33@SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
throws Throwable {
Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
classLoader = AndroidNClassLoader.inject(loader, application);
}
//because in dalvik, if inner class is not the same classloader with it wrapper class.
//it won't fail at dex2opt
if (Build.VERSION.SDK_INT >= 23) {
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, files, dexOptDir);
} else {
V4.install(classLoader, files, dexOptDir);
}
//install done
sPatchDexCount = files.size();
Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
if (!checkDexInstall(classLoader)) {
//reset patch dex
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}
}
}
可以看到 Tinker 对不同系统版本分开做了处理,这里我们就看使用最广泛的 Android4.4到 Android5.1.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/**
* Installer for platform versions 19.
*/
private static final class V19 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
* file entries.
*/
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makeDexElement", e);
throw e;
}
}
}
V19.install()中先通过反射获取 BaseDexClassLoader 中的 dexPathList,然后调 用了 ShareReflectUtil.expandFieldArray().值得一提的是微信对异常的处理很细致, 用 List
接着跟到 shareutil包下的 ShareReflectUtil类,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* Replace the value of a field containing a non null array, by a new array containing the
* elements of the original array plus the elements of extraElements.
*
* @param instance the instance whose field is to be modified.
* @param fieldName the field to modify.
* @param extraElements elements to append at the end of the array.
*/
public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) jlrField.get(instance);
Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length);
// NOTE: changed to copy extraElements first, for patch load first
System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
System.arraycopy(original, 0, combined, extraElements.length, original.length);
jlrField.set(instance, combined);
}
这里的入参fieldName正是上一步中的” dexElements” ,在这么不起眼的一 个工具类中终于找到了 Dex流派的核心方法
和其他Dex流框架的实现几乎一模一样,至此可以看到,虽然微信团队说的天花乱坠, 其本质仍然是用 dexElements中位置靠前的 Dex优先加载类来实现热修复: )
但传统方法为避免不同dex 中类互相引用而报 unexpectedDEX 错引入了” 插桩 ” 的步骤,给每个类加上一行引用其他类的方法,这样导致第一次加载类时耗时变长.应 用启动时通常会加载大量类,所以对启动时间的影响很可观.Tinker 的亮点是通过全量 替换dex 的方式避免 unexpectedDEX,这样做所有的类自然都在同一个 dex 中.但这会带来补 丁包 dex过大的问题,由此微信自研 DexDiff算法取代传统的 BsDiff极大降低了补丁包 大小,又规避了运行性能问题又减小了补丁包大小,可以说是 Dex流派的一大进步.
Robust
美团的热更新方案Robust借鉴了AndroidStudio2.0版本开始提供的功能Instant Run 的实现,另辟蹊径又比上述两种流派在原理上更简单,但同样也有些难以克服的缺陷.
Robust在编译阶段给每个类每个函数插入一段代码,例如1
2
3public long getIndex() {
return 100;
}
被处理成如下的实现:1
2
3
4
5
6
7
8public static ChangeQuickRedirect changeQuickRedirect; public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封装了获取当前 className和methodName的逻辑,并在其
//内部最终调用了 changeQuickRedirect 的对应函数
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
} }
return 100L; }
每个 class都增加了个类型为 ChangeQuickRedirect
的静态成员,而在每个方法前 都插入了使用 changeQuickRedirect 相关的逻辑,当 changeQuickRedirect 不为 null 时,可能会执行到 accessDispatch从而替换掉之前老的逻辑,达到 fix的目的。
如果需将 getIndex 函数的返回值改为 return 106,那么对应生成的 patch,主 要包含两个 class:PatchesInfoImpl.java
和 StatePatch.java
。
PatchesInfoImpl.java:1
2
3
4
5
6public class PatchesInfoImpl implements PatchesInfo { public List<PatchedClassInfo> getPatchedClassesInfo() {
List<PatchedClassInfo> patchedClassesInfos = new ArrayList<PatchedClassInfo>();
PatchedClassInfo patchedClass = new PatchedClassInfo("com.meituan.sample.d", StatePatch.class.getCanonicalName());
patchedClassesInfos.add(patchedClass);
return patchedClassesInfos; }
}
StatePatch.java:1
2
3
4
5
6
7
8
9
10
11
12public class StatePatch implements ChangeQuickRedirect {
public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) { String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return 106; }
return null; }
public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return true; }
return false; }
}
客户端拿到含有 PatchesInfoImpl.java和 StatePatch.java的 patch.dex后,用 DexClassLoader加载 patch.dex,反射拿到 PatchesInfoImpl.java这个 class。拿到后, 创建这个 class的一个对象。然后通过这个对象的 getPatchedClassesInfo函数,知道需 要 patch的 class为 com.meituan.sample.d(com.meituan.sample.State混淆后的 名字),再反射得到当前运行环境中的 com.meituan.sample.d class,将其中的 changeQuickRedirect字段赋值为用 patch.dex中的 StatePatch.java这个 class new出来的对象。这就是打 patch 的主要过程。通过原理分析,其实 Robust 只是在正常的 使用 DexClassLoader,所以可以说这套框架是没有兼容性问题的
这个实现方式的优点是简单、不涉及底层知识,所以易维护.修复能够立即生效无需重 启.但缺点也很明显,给每个类每个函数都插入一段代码将导致安装包体积大增,同时对运 行性能有影响.