你必知的热修复与插件化背景知识

1. 前言

热修复和插件化技术被越来越多的提及,也出现了很多框架。本文并不分析某个具体框架的实现,而是从热修复和插件化想解决的问题入手,从更为宏观的层面梳理了实现这两项技术需要克服的难题及现有的解决思路,有了这些知识作为铺垫可以更好的理解现在市面上一些框架的实现

2. 热修复

2.1 热修复的目的是什么?

开发过程中,我们可能会遇到这样的问题:刚发布的应用就发现了比较严重的 bug,或者有一些小的功能想即时的推送给用户去使用等,热修复的目的就是为了解决这些问题

2.2 实现这个目的有哪些难点要解决?

出现 bug 一般是一个或多个 class 出现了问题,理想的状态下,我们只需修复好这些 class,并更新到用户手机上的 app 中就可以修复这些 bug 了

所以,不管哪种热修复方案,一般总有如下几个步骤:

  1. server 端生成 patch,内含修复好的 class
  2. 下发 patch 到用户手机,即让 app 从服务器上下载(pull 或 push)
  3. app 通过某种方式使 patch 中的 class 被 app 调用

热修复框架主要负责解决第 1 步和第 3 步的问题,一般的,热修复框架都会提供生成 patch 文件的工具,该工具会通过比较的新老 apk 包的差异来生成 patch 文件。除此之外,更加重要的是热修复框架要解决如何使用 patch 中的 class 的问题,而不是再去调用有 bug 的 class

2.3 解决这些难点的思路是什么?

针对第 3 步,当一个 patch 已经下发到手机中后,这个 app 如何去使用 patch 中的 class 的问题?现有的一些解决思路大致可以分为两类:1. 从 ClassLoader 的角度出发 2. 从 Native 层的角度出发

2.3.1 从 ClassLoader 的角度出发

简单粗暴的方案

我们知道,通过 DexClassLoader 可以加载指定目录中的字节码文件,DexClassLoader 在查找一个类时,最终就是遍历一个 Element 的数组,从 Element 中拿到 dexFile 并在 dexFile 中查找特定类的过程,一旦找到了这个类就会返回,不再往下查找了

利用这个思路,假设现在代码中的某一个类或者是某几个类有 bug,那么我们可以在修复完 bug 之后,可以将这些个类打包成一个 patch.dex,然后通过这个 patch.dex 封装出一个 Element 对象,并且将这个 Element 对象插到原有 dexElements 数组的最前端,这样当 DexClassLoader 去加载类时,优先会从我们插入的这个 Element 中找到相应的类,虽然那个有 bug 的类还存在于数组中后面的 Element 中,但它已经没有机会被加载了,这样一个 bug 就在没有重新安装应用的情况下修复了

Tinker 的方案

还有一种从 ClassLoader 入手的策略比较新奇,是 Tinker 提出的

通过修复好的 class.dex 和原有的 class.dex 比较差生差量包补丁文件 patch.dex,在手机上这个 patch.dex 会和原有的 class.dex 合并生成新的文件 fix_class.dex,用这个新的 fix_class.dex 整体替换原有的 dexPathList 的中的内容,这是从根本上把 bug 给干掉了

至于两个 dex 是如何比较得出差异化文件 patch.dex 还有如何合并的,就是 Tinker 的核心算法 DexDiff / DexPatch

值得注意的是:

  • Tinker 可以新增字段、新增类,但不支持新增四大组件
  • 在下次进程启动时 patch 才会生效,patch 安装好后应用会闪退

2.3.2 从 Native 层的角度出发

AndFix 的方案

AndFix 提供了一种在 Native 层实现方法替换的解决方案

AndF 只能修复方法级别的 bug,在比较生成的 patch 中,AndFix 会用注解标注那些修改过的方法。在加载 patch 时,AndFix 首先通过注解找到所有需要被替换的方法,接着通过 jni 的方式在 Native 层对 dex 文件进行操作,实现方法的替换,这种方式可以达到即时生效无需重启的效果。

对于 Native 层具体是如何操作的,由于对 Native 不熟悉,此处略去不表

值得注意的是:

  • AndFix 只能修复方法级别的 bug,不能新增类和字段
  • 由于 AndFix 是在 Native 层进行的操作,而国内各大手机厂商又喜欢定制自己的 ROM,所以很多底层实现的差异,导致 AndFix 的兼容性并不是很好

3. 插件化

3.1 插件化的目的是什么?

随着应用越来越大,会带来各种问题,如:方法数超过 65535,内存消耗很大,编译速度很慢等,插件化就是希望能将一些独立的功能模块提取出来当成插件,以此来减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块

3.2 实现这个目的有哪些难点要解决?

对于插件来说,我们也是当成 application 类型来进行开发的,插件编译后也是生成一个独立的 apk 文件,对于宿主 apk 是经过系统安装的,但是对于插件 apk 是没有安装的,那么关键问题就在于宿主如何才能使用到插件 apk 中的类和资源

也即,插件化需要克服的几个主要难题如下:

  1. 如何加载插件 apk 中的类?
  2. 如何加载插件 apk 中的资源?
  3. 如何管理插件中的四大组件?
  4. 如何处理插件中的 Manifest 文件?

总结起来,一个插件化框架的作用就是要对所有的插件进行管理,其中包括:加载插件中的类、加载插件中的资源、管理插件中的四大组件的生命周期等

3.3 解决这些难点的思路是什么?

3.3.1 插件类的加载

由于宿主 apk 已经安装到系统中,所以系统会自动为它创建对应的 ClassLoader(PathClassLoader,只能加载已安装的 apk 中的 class 文件)。但插件 apk 没有安装,所以系统是不会为它创建 ClassLoader 的,所以插件化框架就需要为每个插件 apk 创建对应的 ClassLoader(DexClassLoader 可以加载指定目录中的 class 文件)

3.3.2 插件资源的加载

我们先来看看 android 中资源是如何被加载的:

可以看出,Resources 只是完成了资源 id 到资源文件名的映射,最终去读取资源文件的是 AssetManager 这个类,Resources 也是依赖 AssetManager 的

由于宿主 apk 已经安装到系统中,所以系统会自动为它创建对应的 AssetManager 和 Resources。但插件 apk 没有安装,所以是没有自己的 AssetManager 和 Resources 这些资源相关的加载类的,所以插件化框架就需要为每个插件 apk 创建对应的 AssetManager 和 Resources。由于 AssetManager 并不是一个 public 的类,所以需要通过反射去创建,并调用它的 addAssetPath() 方法将插件的资源路径加入到 AssetManager 中,以便能够实现对插件资源的访问

3.3.3 插件中四大组件的管理

Android 中的四大组件是一种特殊的类,它们是有生命周期的,并且统一由系统服务 AMS 管理,仅仅构造出这些类的实例是没用的,还需要想办法把它们交给系统管理,让 AMS 赋予组件生命周期

插件化中对四大组件的生命周期管理当属 Activity 最为复杂,下面只介绍对 Activity 生命周期的管理(其他组件略去不表),大致有两种思路:1. ProxyActivity 代理 2. hook 系统启动 Activity 的过程

ProxyActivity 代理

这种方式首先需要通过统一的入口(PluginManager)来启动插件 Activity,其内部会将启动的插件 Activity 信息保存下来,并将 intent 替换为启动 ProxyActivity 的 intent。ProxyActivity 启动后,根据插件的信息创建插件的 ClassLoader 和 Resource,通过反射创建 PluginActivity 并调用其 onCreate() 方法

接下来,PluginActivity 中所有与 context 相关的方法需要委托给 ProxyActivity 的相应方法去实现,而 ProxyActivity 需要将所有生命周期函数及和用户交互相关函数转发给 PluginActivity 去响应

hook 系统启动 Activity 的过程

这个方案其实就是偷梁换柱,当要启动插件的某个 Activity 的时候,先告诉系统我要启动的是 StubActivity,让这个 Activity 进入 AMS 接受校验,暂时骗过系统,然后再在合适的时机又替换回我们需要启动的真正的 Activity

实现上,主要有两步:

  1. 先在 Manifest 中预埋 StubActivity
  2. hook 替换掉系统的 Instrumentation,修改其中的 Activity 的启动和创建这两个过程

至于为什么插件中的 Activity 能正确的收到生命周期回调?原因在于 AMS 是使用一个 token 来与 Activity 交互的,在处理完 Activity 后 AMS 会返回一个 token 给我们的 app,这个 token 是 binder 对象,在 Activity 里面有一个成员变量 mToken 代表的就是它,token 可以唯一地标识一个 Activity 对象,它在 Activity 的 attach() 方法里面初始化,对于 AMS 来说它以为操作的这个 token 是 StubActivity,但在我们的 app 进程里,已经把它替换成了目标 Activity

3.3.4 Manifest 文件的处理

Manifest 文件记录了应用中有哪些组件,Android 启动一个应用程序之前,它必须知道哪些组件是存在的

其实,一个普通的 android 工程依赖 library 工程的时候,这些 library 工程也是有自己的 Manifest 文件的,这时候就有了多个 Manifest 文件,这时 gradle 在构建最后的 apk 时会将 library 中的 Manifest 文件 merge 到我们 app 的 Manifest 文件中

但是这个流程是不会合并插件中的 Manifest 文件的,所以,插件化框架就需要修改整个构建流程,将所有插件中的 Manifest 文件合并到宿主的 Manifest 文件中去

总结起来:

  • 插件类的加载:为每一个插件创建对应的 DexClassLoader
  • 插件资源的加载:反射调用 AssetManager.addAssetPath() 将插件的资源路径加载到 AssetManager 中
  • 四大组件生命周期的管理:ProxyActivity代理、hook
  • Manifiest 文件的处理:修改 gradle 构建流程,合并插件的 Manifest 到宿主的 Manifest 中

4. 最后

我们来比较一下插件化、组件化、热更新等概念:

  • 组件化是为了代码的高度复用性而出现的,它是一种编程思想,指的是把常用的功能模块封装成独立的 library,以供所有想使用它的 apk 使用
  • 插件化是为了解决应用越来越庞大而出现的,它是一种技术手段,指的是将各个关联性不是很强的业务独立的封装到自己的 apk 插件中,通过插件附属到宿主 apk 中完成功能的实现
  • 热更新是为了解决线上 bug 或小功能的更新而出现的,插件化与热更新都是动态加载技术的应用,只是它们的侧重点不同

5. 参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容