一. 插件原理(for shadow)
文中提到的插件 可以简单理解为安卓应用中可以免安装的APK文件。在安卓系统中,安装一个apk就是将其文件解压到设置的相关目录下的操作;启动应用时,再由系统经过一系列处理创建出目标应用的进程并实例化相关的Activity。
我们业务使用的qShadow框架在插件和宿主中提供了一个中间层,使得插件无需在系统中注册也能正常的被启动。
贴两张图比较直观的感受shadow框架,网上的参考资料很多,原理不再过多赘述。
二. shadow加载流程
作为插件,这里的安装和普通的apk安装不太一样。群视频插件这里的安装------是将群视频下载下来的Apk解压到指定地址(宿主内部存储位置),并解析插件信息,然后将插件中的四大组件保存到数据库中
shadow安装这一步,会保存插件的相关信息
// TODO 转换过程
三. 插件与宿主通信方式
插件本身支持独立运行,又因为插件运行在宿主的环境中,天然的支持引用宿主的能力,可以减少不必要的依赖,利于插件体积缩小。但是在代码层面上,插件又和宿主是独立的,无法直接关联,所以我们需要引入中间库,作为桥梁 建立起插件和宿主的代码引用关系。
插件和宿主 都依赖同一版本的bridge库(可以在bridge中定义两端需要的接口和数据格式),这样插件就可以代码编写时 通过bridge直接调用到宿主的能力,宿主也能够通过bridge来给插件发信息。
在这种模式下,需要保证插件和宿主所依赖的bridge版本一致,否则会造成运行时异常。
四. 插件预下载&增量更新
1. 背景:
插件维护阶段,大的需求变更较少,很多时候更新插件版本只是为了解决一些用户反馈的小问题。但即使是很小的更新,都需要插件发布新版本,用户更新-下载-安装这些步骤中,都会造成用户的流失。
我们简单统计了下业务的取消下载情况 // TODO 点击取消的下载率
可以看到,下载这一步有很多取消的情况。所以 有必要对插件进行预下载
插件大小约为27M,此类预下载对用户流量和cdn的影响也不得不考虑。那有没有可能对插件进行热更新呢?只需要下发小的补丁文件即可以达到修复的能力确实更适合我们业务场景。于是我们开始调研现有的热更新能力
优点 | 缺点 | 是否采纳 | |
Sophix | 集成简单有成熟的发布能力 | 对代码有侵入性需要修改Application | ❌ |
bugly tooly | 多引擎融合同Sophox | 宿主也接入了tooly,存在冲突 | ❌ |
插桩 | 性能稳定性好实时生效 | 对包体积影响较大修复范围局限 | ❌ |
可以看到,比较成熟的热更方案都需要对应用的Application进行修改,来完成插件能力的注入。而我们业务作为插件 其所使用的Application是经过桥接的,导致无法在插件的环境中注入热更框架
但是也正是由于是插件,我们的更新流程并不像其他App应用那样复杂(无审核、上架等步骤)。所以 我们试想 可以直接对两个插件安装包进行差分,得到一份补丁文件,用户更新插件时只需要下载这份补丁文件到本地,再和本地的插件文件进行合并,重新得到一份新的插件安装包并替换到插件目录,当用户下次启动插件时,就可以直接加载新的插件了
有了完整的思路,接下来就是处理如何实现了。
1. 处理插件配置文件 - 在插件配置文件中 新增patchInfo字段,用于存放需要patch的插件和补丁包信息
arduino
public static class PatchInfo {
public String version; // patch 对应的版本号
public String url; // patch包地址
public String md5; // patch文件md5
public String length; // patch包大小
}
2. 差分合并方案 - ApkDiffPatch
首先,我们知道 APK本身也是一个压缩文件,所以我们对新旧两个APK进行diff 可以得到 diffData,再将old.apk 加上 diffData,便可以得到patch.apk。不过,我们需要注意以下一些问题:
- 保证补丁尽量小
- patch后得到的patch.apk 和 new.apk 需要完全一致
- 原始数据包不能被改变
基于此,我们采用目前的开源工具ApkDiffPatch 来完成我们的功能,将安装包抽象成未压缩的数据来进行diff,充分发挥diff的优势,获得尽可能小的补丁。(ApkDiffPatch 对文件的diff是基于HDiffPatch库实现的)
同时,为了保证patch生成的包和diff时的new.zip完全一致,我们需要使用 APKNormalized对apk进行预处理
3. 对插件包执行APKNormalized,并且在构建发布包的时候,同时构建补丁文件
使用ApkDiffPatch可以生成需要的patch包,将补丁文件发布到cdn上,并更新内容到定义的patchInfo中,补丁文件中 主要包括 patch的规则和需要patch的diff内容。
4. 修改宿主加载插件方式 支持patch
调整宿主内插件的加载流程,优先下载patch包,并在后台完成文件合并操作
下载patch包到本地后,我们根据规则 进行合并。在patch.conf中,已经定义了patch规则
css
[ {"op":"u", "to":"config.json", "from":"config.json"}, {"op":"u", "to":"my1.apk", "from":"my1.apk"}, {"op":"u", "to":"my2.zip/config.json", "from":"config.json"}, {"op":"p", "to":"old.apk", "from":"patch.hpz", "md5":"A0A7CC326558A14C9AAC0A7F1AE91227"} ]
定义了patch操作
● op的能力
-
u 代表需要将某个文件更新
-
p 代表需要进行patch操作,并且需要校验md5值
● from/to
-
from 表示从当前的patch文件包中读取文件
-
to 表示需要放置到哪个文件的包中
kotlin
private fun patchNormalFiles(zipEntry: ZipEntry, normalOps: List<PatchConfigBean>): Boolean {
for (op in normalOps) {
if (zipEntry.name != op.to) {
continue
}
when (op.op) {
"u" -> update(op.to, op.from, op.md5, filesInPatchStream[op.from])
"d" -> delete(op.to)
else -> {
doPatch(op.op, op.to, op.from, op.md5)
}
}
return true
}
return false
}
5. 完成安装时校验
- 插件信息的更新与校验
根据配置平台配置的patchInfo信息,下载对应的patch文件,对文件进行md5校验
-
patch后安装包的校验
patch后的文件,和 配置平台下发的完整插件信息 进行 长度和 md5校验。
由于插件并不是真正的安装在Android系统中,所以插件目前仅需要V1签名,宿主对插件的校验 主要依赖于 文件MD5 以及 文件长度。
由于我们对插件包用ApkNormalized工具进行了标准化,所以能够保证我们patch时还原的apk md5校验通过
6. 验证
// 待补充线上数据
7. TODO
插件patch的过程比较耗时