如今越来越多的 APP 采用了 apk 文件完整性校验来检测是否被修改,原先 hook 签名数据的方式已无法通过该检测,对于该情况需要使用原包过签方式。
其原理是将原 apk 文件打包到过签包中,运行时将原 apk 文件释放到本地并进行 IO 重定向,实现对抗整包校验,但这也导致了过签包的体积是原体积的两倍以上。
以上是某个过签包的文件结构,assets/base.apk 是原包,其体积占了整个过签包体积的近一半,如果我们对比这个过签包与原包的内容,会发现过签包
新增了:
修改了:
以下文件则完全一样:
如果这 2 个一样的文件有 10 MB,那么它们会在过签包中占据 20 MB 的空间,造成了空间浪费,而这个是可以优化的。
我们知道 APK 文件使用的是 ZIP 格式,而 ZIP 文件末尾有一个中央目录记录了所有的文件头和它们的数据偏移。
经过测试,安卓系统读取 apk 文件时,先读取中央目录来获取所有文件信息,再根据数据偏移读取文件数据。
重点来了,文件数据是通过中央目录的偏移去定位的,而过签包和原包都是 ZIP 结构,那么对于相同的文件,我们可以让过签包和原包的中央目录数据偏移都指向同一个地址,删除另一个地址的数据段,于是就实现了体积优化。
具体如下:
优化前
优化后
因为原包的数据不能动,因此我们删除的是过签包中对应文件的数据段,并让过签包中央目录对应文件的数据偏移指向原包对应的数据段。
这里有个非常重要的前提,原包必须以存储方式打包到过签包中,不能压缩!!!
源码无任何依赖,方便移植与修改,可直接复制到你的项目,并调用以下方法。
DataMultiplexing.optimize("test.apk", "output.apk", "assets/base.apk", true);
V2 / V3 签名必须在优化之后进行,并且必须使用 V2V3SchemeSigner
,否则优化会失效,关于签名的更多信息请看后面的说明。
V2V3SchemeSigner.sign(new File("output.apk"), new JksSignatureKey("test.jks", "123456", "123456", "123456"), true, true);
另外输出的文件已经过 ZipAlign,不建议进行其它处理,否则很可能导致失去优化效果。
根据上面的原理,优化后能减小多少体积取决于过签包与原包有多少完全相同的文件,最多不会超过 50%,这里的完全相同包括文件名相同、文件数据相同、压缩方式相同。
由于该优化要求原包必须以存储方式进行打包,相比以压缩方式打包会占用更多体积,不过原包已经是一个压缩包了,再次压缩效果有限,但极端情况下也可能因为这个导致优化后体积增大。
由于目前基本没有兼容该技术的工具,对优化包进行二次修改(如签名,添加或删除文件等)将大概率导致优化失效,体积会膨胀回去,需要再次进行优化。
打个广告,MT管理器已完美兼容该技术,修改此类 apk 不会导致优化失效。
我们一般使用 apksig 对 apk 进行签名,然而经过测试,使用该库签名后,数据复用优化会失效,而数据复用优化又会破坏 V2 / V3 签名。
为此本项目提供了一个 V2V3SchemeSigner
来进行 V2 / V3 签名同时又不破坏数据复用优化。
在签名时需要遵守以下规则:
先用 apksig 进行 V1 签名,然后进行数据复用优化。
先进行数据复用优化,再使用 V2V3SchemeSigner
进行签名,不需要用到 apksig。
不需要兼容 Android 4.x 的话直接使用这个方案比较方便。
先用 apksig 进行 V1 + V2 / V3 签名,然后进行数据复用优化,最后使用 V2V3SchemeSigner
再次签名。
感谢 LSP 技术团队!该技术思路并非原创,起因是前阵子有网友跟我反馈,使用 MT 修改 LSPatch 生成的 apk 文件后体积会暴涨,在分析了测试文件后才发现了这个思路。