多渠道打包方案调研思考
前言
本来暑假应该是很忙很忙的时间,996的时间安排日子,但是没想到我们竟然出乎意料的闲,等着UI出图做项目,所以最近在闲暇时间之余写个apk系列的文章记录下项目里面临的问题以及整个调研思考的过程
业务情况
-
我们的项目里面有个典型的业务就是多渠道打包,但是这种多渠道打包和传统意义上的多渠道打包有点不一致,我们项目里的多渠道打包,是要根据不同的渠道,有不同的apk名称,app名称,app的logo,以及多个键值对的渠道信息,而且打的包在40-50个左右,而且还在不断地增长中
-
项目中原来使用的多渠道打包方案是Android官方提供的多渠道打包方案productFlavors,这种方式在使用的时候其实也并不是那么舒服,因为接近50个渠道信息,包括app名称,logo,3-5个与渠道相关的键值对信息,完全写在gradle里其实很难受,要定义好对应的sourceSet,以及map kv对应的数据,这样让gradle很臃肿,即便抽出一个独立的gradle apply进去,假如只是要打其中部分渠道的包又会相对比较麻烦
-
最终在1.0版本是采取了xml配置文件的方式进行渠道信息的接入
<channels>
<channel>
<channelName>xxxx</channelName>
<appName>xxxxxx</appName>
<channelInfo1>channelInfo1</channelInfo1>
<channelInfo2>channelInfo2</channelInfo2>
<channelInfo3>channelInfo3</channelInfo3>
</channel>
</channels>
- 以这种xml的方式配置渠道信息,在打部分渠道包的时候把xml的其他不需要打包的渠道注释好就行了,这样其实灵活性不低,但是如果是刚接手项目的小伙伴刚拉下来,build的时候就会慢的飞起,而且这个文件也git管理起来了,这样就导致git管理上挺麻烦的
- 而且productFlavors这种方式终归还是慢,实际测试过打50个我们项目的apk需要40-50min时间,然后发版的网站也没有批量发版的操作,只能一个渠道一个渠道的上传,麻烦的先不说,很容易就把渠道传错,然后就折腾个一番
思考及探索
由于在项目中遇到的坑太多,所以笔者才决定去找一套新的方案去解决上述的问题
当下流行的多渠道打包方案
-
productFlavors: 不予考虑
-
apktool: apktool是Android的一个逆向分析工具,原理其实就是把apk解开,添加渠道信息之后,重新打包成新的apk,具体步骤如下:
- 复制一份新的apk
- 通过apktool 工具,解压apk
- 删除已有签名信息
- 添加渠道信息
- 通过apktool,重新打包生成新apk
- 重新签名
方案点评:这种方案的多渠道打包是切实可行的,但是一开始笔者忽略了这种方案主要是因为网上资料不多,评价也不咋地
-
Walle及VasDolly: 美团多渠道打包方案和腾讯的多渠道打包方案其实在V1签名的处理上有所差异,Walle是添加空文件,VasDolly是添加zipcomment,v2签名就就往signing block里添加id-value的数据
方案点评:这个方案其实当时真的很想很想用在项目里,但是不管是walle还是vaslloy针对的情况都是无需重签名添加渠道信息的,和业务需求格格不入
得到启发
- 当时其实是在apk知识这一块受到了资源混淆框架AndResGuard的影响和启迪,当时资源混淆这一块的设计对我影响很大,当时就萌生了一种想法,AndResGuard混淆的原理其实也是通过修改读取arsc资源表的内容加以操作,其实从AndResGuard中揭示了一个事实,数据存在在apk中,肯定是会有某种方式的,我们只要找到它存在的方式,就可以对它进行操作了
- 因为便萌生了通过修改arsc文件实现符合我们业务情景的多渠道打包方案
技术落地与困难
基于上面的探索和原理,梳理一遍整个多渠道打包的流程
- 解压apk
- 读取manifest中信息,重点是读取application标签中的icon,roundIcon,label,并保存起来,由于manifest已经经过了编译,所以其实这个icon,roundIcon,label都是对应的resource.arsc文件的资源id
- 读取resource.arsc文件的内容,修改label对应id的app名称,保存icon,roundIcon的资源id对应res目录下的资源
- 修改上一步记录的res中的资源文件
- 写回arsc文件
- ps:写回arsc文件和修改res资源文件的两个操作并发执行
- 7zip重压缩
- 重签名
技术落地的过程其实借鉴了微信和美团的设计,其实整个arsc的读取操作都是根据apktool提供的jar包中对arsc文件读取操作实现的,而我自己便在此之上添加写回的操作,笔者认为整个多渠道打包工具的难点在于resource.arsc文件的读取,修改以及写回操作
困难1:resource.arsc读取
读取resource.arsc资源索引表的时候,会出现读多的异常
- 问题原因:其实出现这个问题的原因就是因为笔者在书写读取操作的时候,其实并不想把太多东西保存在内存,而只是想把笔者想要的资源保存起来,所以其实并没有采取apktool中的读取流程,所以在读到最后一个TypeSpec中的Type的时候发现他会读多了
- 解决方案:这个问题的解决方案有很多:
- 直接catch EOF这个异常,因为如果遇到EOF异常就说明肯定读完,但是这样做的话,要把读过的数据保存在内存中,其实对于内存相对较低的情况不太友好
- 还原apktool的做法
- 自实现一种机制去检测读取的位置
困难2:修改resource.arsc中的字符串
- 这里字符串的编码格式踩了坑,由于没注意编码格式导致了出现乱码的状况,最后分别对StringBlock中的utf-8和utf-16编码进行适配与转换
解决代码如下
private byte[] getUTF8Bytes(String s) {
int n = s.length();
byte[] utfBytes = new byte[3 * n];
int k = 0;
for (int i = 0; i < n; i++) {
int m = s.charAt(i);
if (m < 128) {
utfBytes[k++] = (byte) m;
continue;
}
utfBytes[k++] = (byte) (0xe0 | (m >> 12));
utfBytes[k++] = (byte) (0x80 | ((m >> 6) & 0x3f));
utfBytes[k++] = (byte) (0x80 | (m & 0x3f));
}
if (k < utfBytes.length) {
byte[] tmp = new byte[k];
System.arraycopy(utfBytes, 0, tmp, 0, k);
return tmp;
}
return utfBytes;
}
- 重新写入工作的繁琐,由于是一个字节一个字节,其实容错率并不高,解决代码如下
public int overwriteString(int index, String change) {
int diff = 0;
if (index >= 0 && this.stringOffsets != null && index < this.stringOffsets.length) {
int offset = this.stringOffsets[index];
int stringOffset;
int oldLength;
int[] val;
byte[] changeBytes;
if (this.isUTF8) {
val = getUtf8(strings, offset);
stringOffset = val[0];
oldLength = val[1];
changeBytes = getUTF8Bytes(change);
} else {
val = getUtf16(strings, offset);
stringOffset = val[0];
oldLength = val[1];
changeBytes = new String(
change.getBytes(StandardCharsets.UTF_16),
StandardCharsets.UTF_16).getBytes();
}
if (oldLength == changeBytes.length) {
for (int i = stringOffset, j = 0; j < changeBytes.length; i++, j++) {
strings[i] = changeBytes[j];
}
} else {
diff = changeBytes.length - oldLength;
handleStringsSizeChange(offset, diff, index, oldLength, changeBytes);
}
}
return diff;
}
困难3:4的倍数
当时经过漫长的代码书写以及测试后发现,apk中的resource.arsc修改成功了,重签名也成功,用apktool解析也没有问题,但是就是安装不了
- 当时解决这个问题,最初的想法其实是看PMS安装的源码和launcher显示图标的源码,分析写入操作到底是哪里出错
- 然后读完了源码发现没有找到我要的答案,本想去读AssetManager读取资源的源码,但是,这个时候我在AndResGuard中再次得到了启发
在AndResGuard中有一段很关键的源码
int size = (chunkSize - stylesOffset);
if ((size % 4) != 0) {
throw new IOException("Style data size is not multiple of 4 (" + size + ").");
}
这段源码给了我问题的答案啊,其实不是写入有问题,只是没有把StringBlock中的字符串数目保持在4的倍数,导致安装失败
解决方案: 修改完resource.arsc文件的app label后判断一下长度,不是4的倍数便补0对齐
小结
笔者在项目中遇到的困难不止于此,只列举了3个耗时最长的案例
未来期望
- 虽然实现了功能,外部调用也是采用gradle的方式,但是对于内部的设计笔者还是不太满意,想通过进一步学习walle和vaslloy的源码加以改造原来的设计
- 整个打包方案其实最耗时的是签名操作,所以笔者想添加上dex加壳,并尝试把壳dex中的注释信息删掉,尝试做到加固以及apk瘦身
- 关于AndResGuard:其实真的从这个框架得到不少启发,甚是感谢,但是没有办法把这个框架导入到项目中,挺可惜的,因为项目推广的人员一直想在项目里添加换肤功能,如果把AndResGuard添加上去会比较蛋疼