进阶高工必备技能:Android热修复技术全解析!(附视频+电子书分享)

本文对比了美团的Robust、大众点评的Nuwa、饿了么的Amigo以及Tinker等热修复技术方案,讨论了如何根据项目需求、学习成本和公司的技术实力选择合适的解决方案,强调了不同方案的优缺点,如代码侵入性、兼容性和性能影响。
摘要由CSDN通过智能技术生成

3、其他

| 名称 | 说明 |

| — | — |

| Robust | 美团, 开源,实时修复 |

| Nuwa | 大众点评,开源,冷启动修复 |

| Amigo | 饿了么,开源,冷启动修复 |

各热修复方案对比

怎么选择合适的热修复方案

怎么选?这个只能说一切看需求。如果公司综合实力强,完全考虑自研都没问题,但需要综合考虑成本及维护。下面给出2点建议,如下:

  1. 项目需求
  • 只需要简单的方法级别Bug修复?

  • 需要资源及so库的修复?

  • 对平台兼容性要求及成功率要求?

  • 有需求对分发进行控制,对监控数据进行统计,补丁包进行管理?

  • 公司资源是否支持商业付费?

  1. 学习及使用成本
  • 集成难度

  • 代码侵入性

  • 调试维护

  1. 选择大厂
  • 技术性能有保障

  • 有专人维护

  • 热度高,开源社区活跃

  1. 如果考虑付费,推荐选择阿里的Sophix,Sophix是综合优化的产物,功能完善、开发简单透明、提供分发及监控管理。如果不考虑付费,只需支持方法级别的Bug修复,不支持资源及so,推荐使用Robust。如果考虑需要同时支持资源及so,推荐使用Tinker。最后如果公司综合实力强,可考虑自研,灵活性及可控制最强。

热修复技术方案原理

技术分类

image

NativeHook 原理

原理及实现

NativeHook的原理是直接在native层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能。

下面以AndFix的一段jni代码来进行说明,如下:

void replace_6_0(JNIEnv* env, jobject src, jobject dest) {

// 通过Method对象得到底层Java函数对应ArtMethod的真实地址

art::mirror::ArtMethod* smeth =

(art::mirror::ArtMethod*) env->FromReflectedMethod(src);

art::mirror::ArtMethod* dmeth =

(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

reinterpret_castart::mirror::Class*(dmeth->declaring_class_)->class_loader_ =

reinterpret_castart::mirror::Class*(smeth->declaring_class_)->class_loader_; //for plugin classloader

reinterpret_castart::mirror::Class*(dmeth->declaring_class_)->clinit_thread_id_ =

reinterpret_castart::mirror::Class*(smeth->declaring_class_)->clinit_thread_id_;

reinterpret_castart::mirror::Class*(dmeth->declaring_class_)->status_ = reinterpret_castart::mirror::Class*(smeth->declaring_class_)->status_-1;

//for reflection invoke

reinterpret_castart::mirror::Class*(dmeth->declaring_class_)->super_class_ = 0;

//把旧函数的所有成员变量都替换为新函数的

smeth->declaring_class_ = dmeth->declaring_class_;

smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;

smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;

smeth->access_flags_ = dmeth->access_flags_ | 0x0001;

smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;

smeth->dex_method_index_ = dmeth->dex_method_index_;

smeth->method_index_ = dmeth->method_index_;

smeth->ptr_sized_fields_.entry_point_from_interpreter_ =

dmeth->ptr_sized_fields_.entry_point_from_interpreter_;

smeth->ptr_sized_fields_.entry_point_from_jni_ =

dmeth->ptr_sized_fields_.entry_point_from_jni_;

smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =

dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;

LOGD(“replace_6_0: %d , %d”,

smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,

dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);

}

void setFieldFlag_6_0(JNIEnv* env, jobject field) {

art::mirror::ArtField* artField =

(art::mirror::ArtField*) env->FromReflectedField(field);

artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;

LOGD("setFieldFlag_6_0: %d ", artField->access_flags_);

}

每一个Java方法在art中都对应一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括访问权限及代码执行地址等。通过env->FromReflectedMethod得到方法对应的ArtMethod的真正开始地址,然后强转为ArtMethod指针,从而对其所有成员进行修改。

这样以后调用这个方法时就会直接走到新方法的实现中,达到热修复的效果。

优点

  • 即时生效

  • 没有性能开销,不需要任何编辑器的插桩或代码改写

缺点

  • 存在稳定及兼容性问题。ArtMethod的结构基本参考Google开源的代码,各大厂商的ROM都可能有所改动,可能导致结构不一致,修复失败。

  • 无法增加变量及类,只能修复方法级别的Bug,无法做到新功能的发布

javaHook 原理

原理及实现

以美团的Robust为例,Robust 的原理可以简单描述为:

1、打基础包时插桩,在每个方法前插入一段类型为 ChangeQuickRedirect 静态变量的逻辑,插入过程对业务开发是完全透明

2、加载补丁时,从补丁包中读取要替换的类及具体替换的方法实现,新建ClassLoader加载补丁dex。当changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的

下面通过Robust的源码来进行分析。

首先看一下打基础包是插入的代码逻辑,如下:

public static ChangeQuickRedirect u;

protected void onCreate(Bundle bundle) {

//为每个方法自动插入修复逻辑代码,如果ChangeQuickRedirect为空则不执行

if (u != null) {

if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {

PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);

return;

}

}

super.onCreate(bundle);

}

Robust的核心修复源码如下:

public class PatchExecutor extends Thread {

@Override

public void run() {

applyPatchList(patches);

}

/**

  • 应用补丁列表

*/

protected void applyPatchList(List patches) {

for (Patch p : patches) {

currentPatchResult = patch(context, p);

}

}

/**

  • 核心修复源码

*/

protected boolean patch(Context context, Patch patch) {

//新建ClassLoader

DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),

null, PatchExecutor.class.getClassLoader());

patch.delete(patch.getTempPath());

try {

patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());

patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();

} catch (Throwable t) {

}

//通过遍历其中的类信息进而反射修改其中 ChangeQuickRedirect 对象的值

for (PatchedClassInfo patchedClassInfo : patchedClasses) {

try {

oldClass = classLoader.loadClass(patchedClassName.trim());

Field[] fields = oldClass.getDeclaredFields();

for (Field field : fields) {

if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {

changeQuickRedirectField = field;

break;

}

}

try {

patchClass = classLoader.loadClass(patchClassName);

Object patchObject = patchClass.newInstance();

changeQuickRedirectField.setAccessible(true);

changeQuickRedirectField.set(null, patchObject);

} catch (Throwable t) {

}

} catch (Throwable t) {

}

}

return true;

}

}

优点

  • 高兼容性(Robust只是在正常的使用DexClassLoader)、高稳定性,修复成功率高达99.9%

  • 补丁实时生效,不需要重新启动

  • 支持方法级别的修复,包括静态方法

  • 支持增加方法和类

  • 支持ProGuard的混淆、内联、优化等操作

缺点

  • 代码是侵入式的,会在原有的类中加入相关代码

  • so和资源的替换暂时不支持

  • 会增大apk的体积,平均一个函数会比原来增加17.47个字节,10万个函数会增加1.67M

java mulitdex 原理

原理及实现

Android内部使用的是BaseDexClassLoader、PathClassLoader、DexClassLoader三个类加载器实现从DEX文件中读取类数据,其中PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader实现。dex文件转换成dexFile对象,存入Element[]数组,findclass顺序遍历Element数组获取DexFile,然后执行DexFile的findclass。源码如下:

// 加载名字为name的class对象

public Class findClass(String name, List suppressed) {

// 遍历从dexPath查询到的dex和资源Element

for (Element element : dexElements) {

DexFile dex = element.dexFile;

// 如果当前的Element是dex文件元素

if (dex != null) {

// 使用DexFile.loadClassBinaryName加载类

Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);

if (clazz != null) {

return clazz;

}

}

}

if (dexElementsSuppressedExceptions != null) {

suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));

}

return null;

}

所以此方案的原理是Hook了ClassLoader.pathList.dexElements[],将补丁的dex插入到数组的最前端。因为ClassLoader的findClass是通过遍历dexElements[]中的dex来寻找类的。所以会优先查找到修复的类。从而达到修复的效果。

下面使用Nuwa的关键实现源码进行说明如下:

public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {

//新建一个ClassLoader加载补丁Dex

DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());

//反射获取旧DexElements数组

Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));

//反射获取补丁DexElements数组

Object newDexElements = getDexElements(getPathList(dexClassLoader));

//合并,将新数组的Element插入到最前面

Object allDexElements = combineArray(newDexElements, baseDexElements);

Object pathList = getPathList(getPathClassLoader());

//更新旧ClassLoader中的Element数组

ReflectionUtils.setField(pathList, pathList.getClass(), “dexElements”, allDexElements);

}

private static PathClassLoader getPathClassLoader() {

PathClassLoader pathClassLoader = (PathClassLoader) DexUtils.class.getClassLoader();

return pathClassLoader;

}

private static Object getDexElements(Object paramObject)

throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {

return ReflectionUtils.getField(paramObject, paramObject.getClass(), “dexElements”);

}

private static Object getPathList(Object baseDexClassLoader)

throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {

return ReflectionUtils.getField(baseDexClassLoader, Class.forName(“dalvik.system.BaseDexClassLoader”), “pathList”);

}

private static Object combineArray(Object firstArray, Object secondArray) {

Class<?> localClass = firstArray.getClass().getComponentType();

int firstArrayLength = Array.getLength(firstArray);

int allLength = firstArrayLength + Array.getLength(secondArray);

Object result = Array.newInstance(localClass, allLength);

for (int k = 0; k < allLength; ++k) {

if (k < firstArrayLength) {

Array.set(result, k, Array.get(firstArray, k));

} else {

Array.set(result, k, Array.get(secondArray, k - firstArrayLength));

}

}

return result;

}

优点

  • 不需要考虑对dalvik虚拟机和art虚拟机做适配

  • 代码是非侵入式的,对apk体积影响不大

缺点

  • 需要下次启动才修复

  • 性能损耗大,为了避免类被加上CLASS_ISPREVERIFIED,使用插桩,单独放一个帮助类在独立的dex中让其他类调用。

dex替换

原理及实现

为了避免dex插桩带来的性能损耗,dex替换采取另外的方式。原理是提供dex差量包,整体替换dex的方案。差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并成一个完整的dex,完整dex加载得到dexFile对象作为参数构建一个Element对象然后整体替换掉旧的dex-Elements数组。

这也是微信Tinker采用的方案,并且Tinker自研了DexDiff/DexMerge算法。Tinker还支持资源和So包的更新,So补丁包使用BsDiff来生成,资源补丁包直接使用文件md5对比来生成,针对资源比较大的(默认大于100KB属于大文件)会使用BsDiff来对文件生成差量补丁。

下面我们关键看看Tinker的实现源码,当然具体的实现算法很复杂,我们只看关键的实现,最后的修复在UpgradePatch中的tryPatch方法,如下:

@Override

public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {

//省略一堆校验

… …

//下面是关键的diff算法及合并实现,实现相对复杂,感兴趣可以再仔细阅读源码

//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process

if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {

TinkerLog.e(TAG, “UpgradePatch tryPatch:new patch recover, try patch dex failed”);

return false;

}

if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {

TinkerLog.e(TAG, “UpgradePatch tryPatch:new patch recover, try patch library failed”);

return false;

}

if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {

TinkerLog.e(TAG, “UpgradePatch tryPatch:new patch recover, try patch resource failed”);

return false;

}

// check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oat to interpreted

if (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {

TinkerLog.e(TAG, “UpgradePatch tryPatch:new patch recover, check dex opt file failed”);

return false;

}

if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) {

TinkerLog.e(TAG, “UpgradePatch tryPatch:new patch recover, rewrite patch info failed”);

manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);

return false;

}

TinkerLog.w(TAG, “UpgradePatch tryPatch: done, it is ok”);

return true;

}

优点

  • 兼容性高

  • 补丁小

  • 开发透明,代码非侵入式

缺点

  • 冷启动修复,下次启动修复

  • Dex合并内存消耗在vm head上,容易OOM,最后导致合并失败

总结

找工作是个很辛苦的事情,而且一般周期都比较长,有时候既看个人技术,也看运气。第一次找工作,最后的结果虽然不尽如人意,不过收获远比offer大。接下来就是针对自己的不足,好好努力了。

最后为了节约大家的时间,我把我学习所用的资料和面试遇到的问题和答案都整理成了PDF文档

喜欢文章的话请关注、点赞、转发 谢谢!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

hInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);

return false;

}

TinkerLog.w(TAG, “UpgradePatch tryPatch: done, it is ok”);

return true;

}

优点

  • 兼容性高

  • 补丁小

  • 开发透明,代码非侵入式

缺点

  • 冷启动修复,下次启动修复

  • Dex合并内存消耗在vm head上,容易OOM,最后导致合并失败

总结

找工作是个很辛苦的事情,而且一般周期都比较长,有时候既看个人技术,也看运气。第一次找工作,最后的结果虽然不尽如人意,不过收获远比offer大。接下来就是针对自己的不足,好好努力了。

最后为了节约大家的时间,我把我学习所用的资料和面试遇到的问题和答案都整理成了PDF文档

喜欢文章的话请关注、点赞、转发 谢谢!

[外链图片转存中…(img-dQ1D8uy1-1714700510375)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值