【Android进阶笔记】热修复(代码、资源、动态链接库)

1. 热修复

=========================================================================

1.1. 作用


  • 严重的Bug,需要立即解决,而不用重新打包上架。

  • 解决版本升级率不高,Bug会一直影响不升级版本的用户。

  • 实现小功能短时间版本覆盖,如节日活动。

1.2. 主流热修复框架


1.2.1. 主流框架

| 派系 | 框架 |

| — | — |

| 阿里系 | AndFix⚠️、Dexposed⚠️、HotFix⚠️、Sophix |

| 腾讯系 | Tinker、超级补丁、QFix |

| 知名公司 | 美团Robust饿了么Amigo⚠️、蘑菇街Aceso⚠️ |

| 其他 | RocooFix⚠️、Nuwa⚠️、AnoleFix⚠️ |

1.2.2. 框架对比

| 特性 | Sophix | Tinker | 超级补丁 | Robust |

| — | — | — | — | — |

| 即时生效 | 支持 | 不支持 | 不支持 | 不支持 |

| 方法替换 | 支持 | 支持 | 支持 | 支持 |

| 类替换 | 支持 | 支持 | 支持 | 不支持 |

| 类结构修改 | 支持 | 支持 | 不支持 | 不支持 |

| 资源替换 | 支持 | 支持 | 支持 | 不支持 |

| so替换 | 支持 | 支持 | 不支持 | 不支持 |

| 补丁包大小 | 较小 | 较小 | 较大 | 一般 |

| 性能损耗 | 较小 | 较大 | 较大 | 较小 |

| 侵入式打包 | 无侵入 | 侵入 | 侵入 | 侵入 |


2. 代码修复

==========================================================================

代码修复主要有3中方案:类加载方案、底层替换方案、Instant Run方案。

2.1. 类加载方案


采用类加载方案的主要是 Tinker、超级补丁、QFix、Amigo 和 Nuwa 等。

2.1.1. Dex 分包机制

【方法数 65536 限制】

由于 DVM 指令集的方法调用指令 invoke-kind 索引为16bits,即最多能引用 65535 个方法。因此,当一个 dex 文件中法方法数超过 65535 个时,就会抛出编译期异常:com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536

【LinearAlloc 限制】

DVM 中的 LinearAlloc 是一个固定的缓存区,当方法数超出了缓存区的大小时会报错。因此,在安装应用时可能会提示 INSTALL_FAILED_DEXOPT

【分包方案】

  • 解决 65536 限制和 LinearAlloc 限制。

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

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

2.1.2. 类加载

在 Android 的类加载过程中,一个重要环节是调用 DexPathListfindClass 方法。

// 每一个dex文件,都对应一个Element元素,并有序排列

private Element[] dexElements;

public Class<?> findClass(String name, List suppressed) {

// 依次遍历dex文件数组

for (Element element : dexElements) {

// element.findClass内部会调用DexFile的loadClassBinaryName方法查找类

Class<?> clazz = element.findClass(name, definingContext, suppressed);

// 找到了这个类,就返回

if (clazz != null) {

return clazz;

}

}

if (dexElementsSuppressedExceptions != null) {

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

}

return null;

}

2.1.3. 修复方案

根据类的查找流程:

  • 将有 Bug 的类 Key.class 进行修改,再将 Key.class 打包成补丁包 Patch.dex

  • Patch.dex 放在 dexElements 数组的第一个元素。

  • 根据双亲委派,会首先找到 Patch.dex 中的 Key.class 会优先加载,而存在 Bug 的 Key.class 就不会被加载。

具体到实现细节上,不同的框架就有些差异了。

  • 超级补丁:将 Patch.dex 放到 dexElements 数组的第一个元素。

  • Tinker:将新旧 apk 做 diff,得到一个 patch.dex,再将 patch.dex 与手机 apk 中的 classes.dex 进行合并,生成 fix_classess.dex,再将 fix_classess.dex 放到 dexElements 数组的第一个元素。

2.2. 底层替换方案


采用底层替换方案的主要是 AndFix、Dexposed、HotFix 和 Sophix。

优点:不需要重启 APP,立即生效。

2.2.1. ArtMethod

Java 层的每个方法在 ART 中都对应着一个 ArtMethod 的结构体(包含 Java 方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等),只要把原方法的结构体内容替换为新的结构体内容,则在调用原来方法的时候,真正执行的指令是新方法的指令,就可以实现热修复。

art/runtime/art_method.h 文件中,定义了 ArtMethod 的结构体内容。

class ArtMethod FINAL {

/* … */

GcRootmirror::Class declaring_class_;

std::atomicstd::uint32_t access_flags_;

uint32_t dex_method_index_;

uint16_t method_index_;

uint16_t hotness_count_;

uint16_t imt_index_;

struct PtrSizedFields {

ArtMethod** dex_cache_resolved_methods_;

void* data_;

void* entry_point_from_quick_compiled_code_;

} ptr_sized_fields_;

}

2.2.2. 修复方案

  • 方法①:将待修复的 java 方法对应的 ArtMethod 结构体中的每个字段进行替换。

  • 方法②:将待修复的 java 方法对应的 ArtMethod 结构体整个进行替换。

不同的框架采用了不同方案:

  • AndFix 采用方法①,不同版本和不同厂商 ArtMethod 可能不同,存在兼容问题,导致方法替换失败。

  • Sophix 采用方法②,不存在兼容问题 。

无论采用哪种方案,由于类加载后,类的结构和方法数量就已经固定了,因此该方案有以下不适场景:

  • 增加或减少方法和字段的个数。

  • 改变原有类的结构。

Sophix 结合了底层替换方案和类加载方案各自的优点,以底层替换方案为主,类加载方案为辅,在热部署无法使用的情况下,自动降级为冷部署。

2.3. Instant Run 方案


在 Android Studio 2.0 版本上,支持了一个新特性 Instant Run,实现了对代码修改的实时生效(热插拔)。

采用 Instant Run 方案的主要是 Robust 和 Aceso。

2.3.1. Instant Run 原理

在第一次构建 Apk 时:

  • 在每一个类中注入了一个 $change 的成员变量,它实现了 IncrementalChange 接口。

  • 在每一个方法的第一行,插入了一段判断执行逻辑。

public class TestActivity {

// 注入一个类型为IncrementalChange的成员

IncrementalChange localIncrementalChange = $change;

public void onCreate(Bundle savedInstanceState){

// 当localIncrementalChange不为null时,可能会执行到access$dispatch从而替换掉之前老的逻辑

if (localIncrementalChange != null) {

localIncrementalChange.access$dispatch(

“onCreate.(Landroid/os/Bundle;)V”, new Object[] { this, paramBundle });

return;

}

super.onCreate(savedInstanceState);

}

}

当我们点击 Android Studio 的 InstantRun 按钮时:

  • 如果方法没有变化,则 $changenull,执行方法中的旧逻辑。

  • 如果方法有变化,则:

  • 动态生成替换类 TestActivity$overrideAppPatchesLoaderImpl 类。

  • AppPatchesLoaderImpl 类的 getPatchedClasses 方法会返回被修改的类的列表,根据这个列表,TestActivity 中的 $change 会被赋值为 TestActivity$override

  • 判断条件成立,access$dispatch() 方法会执行 TestActivity$override 类中的 onCreate 方法,从而实现对现有 onCreate 方法的修改。

2.3.2. 修复方案

Robust 为例

  • 在编译打包阶段对每个方法都自动的插入了一段代码。

  • 动态下发包含有 PatchesInfoImpl.javaPatch.javapatch.dex 到客户端,用 DexClassLoader 加载 patch.dex,反射拿到 PatchesInfoImpl.java 这个 class 并创建对象。

  • 然后通过这个对象的 getPatchedClassesInfo 方法,获得需要修复的 class 的混淆后名字,再反射得到当前运行环境中的该 class。

  • 其中的 changeQuickRedirect 字段赋值为用 patch.dex 中的 Patch.java 这个 class new 出来的对象。


3. 资源修复

==========================================================================

很多热修复框架的资源修复都参考了 Instant Run 的资源修复原理。由于 Instant Run 不是 Android 的源码,需要反编译才能知道。

Instant Run 资源修复的核心逻辑在 MonkeyPatcher 类的 monkeyPatchExistingResources 方法中。

public class MonkeyPatcher {

public static void monkeyPatchExistingResources(

Context context, String externalResourceFile, Collection activities) {

if (externalResourceFile == null) {

return;

}

try {

// 反射创建新的AssetManager

AssetManager newAssetManager = AssetManager.class.getConstructor(

new Class[0]).newInstance(new Object[0]);

Method mAddAssetPath = AssetManager.class.getDeclaredMethod(

“addAssetPath”, new Class[]{String.class});

mAddAssetPath.setAccessible(true);

// 反射调用addAssetPath方法加载外部资源

if (((Integer) mAddAssetPath.invoke(

newAssetManager, new Object[]{externalResourceFile})).intValue() == 0) {

throw new IllegalStateException(“Could not create new AssetManager”);

}

Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod(

“ensureStringBlocks”, new Class[0]);

mEnsureStringBlocks.setAccessible(true);

mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);

if (activities != null) {

for (Activity activity : activities) {

Resources resources = activity.getResources();

try {

// 把Resources中的mAssets替换为newAssetManager

Field mAssets = Resources.class.getDeclaredField(“mAssets”);

mAssets.setAccessible(true);

mAssets.set(resources, newAssetManager);

} catch (Throwable ignore) {

/* … */

}

// 获取Activity的主题

Resources.Theme theme = activity.getTheme();

try {

try {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

面试复习笔记

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960页Android开发笔记》

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

《960页Android开发笔记》**

[外链图片转存中…(img-wLJ0Cznf-1713434802080)]

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

[外链图片转存中…(img-1wvMRy0d-1713434802081)]

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

[外链图片转存中…(img-6NAfZfaP-1713434802082)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值