android增量更新其实原理很简单,主要是利用bsdiff和bspatch来完成,当然也有其他方案,不过使用最多的还是bsdiff方案,本文也将就此方案展开研究。主要分为以下四个方面:增量更新的实现、增量更新面临的问题、多渠道包解决方案 和 增量升级加断点续传SDK的封装。
1. 增量更新的实现
首先明确一点,patch的生成是在服务器端完成,patch的合成是在客户端完成。
1.1 生成差量包的过程
生成差量包使用bsdiff工具,需要服务器中安装bsdiff命令,测试发现Centos和Ubuntu均可以直接安装bsdiff命令,当然了如果自己用Windows学习用的话,网上很容易下载到bsdiff.exe工具。
bsdiff oldFilePath newFilePath patchFilePath
只需要指定新包、旧包和差量包的路径就完成了,非常简单。
1.2 差量包合成的过程
差量包合成过程相对麻烦一些,要解决两个问题,bspatch工具的制作和本地运行apk的获取。因为在客户端完成合成过程,所以我们无法使用工具,只能下载源码自己编译生成so供程序调用。由于本文篇幅问题,不打算细讲so的生成过程,很多文章写得很清楚,不清楚的读者可以参考鸿洋大神的Android增量更新完全解析。此外,如果想用eclipse编译so,可以参考这篇文章史上最易懂的Android jni开发资料--NDK环境搭建
本地运行APK的安装包的获取
ApplicationInfo applicationInfo = context.getApplicationInfo();
String apkPath = applicationInfo.sourceDir;
就可以拿到运行的apk的路径,可以获取到运行时的apk。接下来就可以体验增量更新的效果啦。
其实关于增量更新的实现部分,最近确实出现很多文章,我就不多废话了,参考鸿洋大神的那篇博客已经足够了。
2. 增量更新面临的问题
其实我最早编译这个so的时候,当时关于增量更新的文章并不多,几乎有个一天就实现了,但是在应用发布的时候确遇到了很棘手的问题------多渠道包的问题。应用为了发布到各个应用市场,在生成release包后,往往会根据渠道文件,生成n多个渠道包,一般都会有上百个甚至上千个渠道包。
在服务器端经过测试利用bsdiff指令对20M的apk做差量包,时间约为20s~30s,如果成百上千个差量包呢?考虑到各种打包失败现象,这个时间负责打包的同事根本无法忍受。此外,最要命的是如果一个渠道对应打一个差量包,那是相当占用服务器空间,更头疼的是维护起来非常困难。
我们知道一般应用的版本号都是三位,例如版本号为4.17.1,很多公司如果小幅修改是不会提示用户更新的,但是服务器上版本会变为4.17.2,此时要不要打差量包?头疼,而且如果要是升级到4.18.0了,即使只针对4.17.x打差量包,那也是非常大的一笔开销。
这个多渠道包的问题困扰了我几个月,一直没想到好的方案解决,也就致使增量更新的功能比较鸡肋。记得在郭霖还是鸿洋的公众号里提问过类似问题,一个作者说增量更新只适合对自家渠道做,如果做多渠道没什么意义。但是探索了这么久,觉得如果不支持多渠道包,总觉得不完美,展开了下一步的探索。
3. 多渠道包解决方案
首先说一下常见的打渠道包的方式,各人觉得这篇文章介绍的比较好,不清楚的同学可以查看下,深入浅出Android打包。
之前公司使用的是类似美团的打包方案。
就是向META-INF中插入一个带渠道标识的空文件。这种方式虽然只插入了一个空文件,但是整个包的md5会改变,bsdiff之后会当做不同的apk包处理。
下面说一下另外一种添加渠道的方式。
Android应用使用的Apk文件就是一个代签名信息的zip文件,,每个文件的最后都必须有一个叫Central Directory record的部分,这个cdr的最后部分叫“End of Central Directory Record”
注释包含Comment Length和FileComment两个字段。前者表示注释的长度,后者表示注释的内容,正确修改这一部分不会对zip文件造成破坏,利用这个字段,我们可以添加一些自定义数据。
apk安装包打包完成后, Comment length默认为0,comment为空,我们可以把渠道号写入comment字段,此时Coment length的长度就是渠道的长度。即可完成打渠道包的过程。这和增量更新有什么关系呢?
这里解释一下。几种打渠道包方式其他方面的利弊我们先不谈,在comment区添加字段的方式的好处就是可以追加内容,可以去除内容,但是不影响zip包完整性,而在META-INF添加新字段的方式不会影响签名,但是会破坏zip包完整性。由这一点想到我们可不可以在服务器端只对新旧版本的原始包打差量包,在本地合成时,将运行的apk拷贝出来后,去掉渠道号内容,然后再合成,得到的就是新的原始包,然后再追加相应的渠道号。
下面给出这种思想的具体实现:
signature占5个字节,自己也可以随意定义,是为了做校验。ChannelLength字段占两个字节,用于记录渠道信息的长度。
下面拿具体实例加以说明。首先准备一个xxx.apk,没有添加渠道信息。
可见最后两个字节的位置为0x00 0x00,就是comment length字段为0
现在假设我们对原始包apk进行写入操作,假设渠道信息为baidu,则channelLengh字段的值为5,后面签名信息为任意5个字节,{0x32, 0x66, 0x56, 0x17, 0x69},执行写入操作后,我们观察下二进制信息。
再次观察字节码发现,commentlength字段值变为了0x0c,即12个字节,接下来是baidu字符串对应的字节,接着是channel的长度0x05,即五个字节,最后是五个字节的签名信息。可以看到结果和设想的完全一样。
思想有了,接下来进行具体的实现。
服务器端写入渠道信息代码如下:
public static final byte[] SIGNATURE = new byte[] { 0x32, 0x66, 0x56, 0x17, 0x69 };
private int writeChannel(File file, String channel) {
try { byte[] data = channel.getBytes("UTF-8");
final RandomAccessFile raf = new RandomAccessFile(file, "rw");
int SHORT_LENGTH = 2;
raf.seek(file.length() - SHORT_LENGTH);
writeShort(data.length + SHORT_LENGTH + SIGNATURE.length, raf);
raf.write(data);
writeShort(data.length, raf);
raf.write(SIGNATURE);
raf.close();
return 0;
} catch (Exception e) {
return -1;
}
}
客户端合成差量包过程核心代码。
private int removeChannel(String apkPath) {
RandomAccessFile accessFile;
byte[] buffer = new byte[SIGNATURE.length];
try { accessFile = new RandomAccessFile(apkPath, "rw");
long index = accessFile.length();
index -= SIGNATURE.length;
accessFile.seek(index);
accessFile.readFully(buffer);
if (!isSignatureMatched(buffer)) {
return -1;
}
index -= CHANNEL_LENGTH;
accessFile.seek(index);
int length = readChannelLength(accessFile);
index -= length + 2;
accessFile.seek(index);
accessFile.write(0x00);//核心代码,将commentLength置0
accessFile.write(0x00);
accessFile.setLength(index + 2);
accessFile.close(); return 0;
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
进行签名验证
private boolean isSignatureMatched(byte[] buffer) {
if (buffer.length != SIGNATURE.length) {
return false;
}
for (int i = 0; i < SIGNATURE.length; ++i) {
if (buffer[i] != SIGNATURE[i]) {
return false;
}
}
return true;
}
时间不够,会抽时间补充完整。
目前本人在公司负责热修复相关的工作,主要是基于robust的热修复相关工作。感兴趣的同学欢迎进群交流。