内容简介:这一次的原理分析仅仅针对在上一家公司的时候,笔者所在的该文章先从安装包
这一次的原理分析仅仅针对 Android Signature V2 Scheme
。
在上一家公司的时候,笔者所在的 Android
团队经历了 Android Signature V1
到 Android Signature V2
的变更,其中因为未及时从 V1
升级到 V2
而导致上线受阻,当时也紧急更换了新的多渠道打包 工具 来解决问题。在我自己使用多渠道打包工具时,不免对 V2
签名验证的方式有了一丝好奇,想去看看 V2
签名验证和多渠道打包的实现原理。
该文章先从安装包 V2
签名验证入手,再从打包过程中分析 Walle
是怎么绕过签名验证在安装包上加入渠道信息,最后看 Walle
怎么从应用中读取渠道信息。在这里我就不讲 Walle
的使用了,建议读者在看原理前先了解一下使用方式。
二、APK Signature Scheme v2
APK Signature Scheme v2
的签名验证,我们先从官方一张图入手
一般情况下,我们用到的 zip
格式由三个部分组成:文件数据区+中央目录结构+中央目录结束标志,分别对应上图的 Contents Of ZIP entries
、 Central Directory``、End of Central Directory
(下文简称为 EOCD
)。正如图中 After signing
所示, APK Signature Scheme v2
是在ZIP文件格式的 Central Directory
区块所在文件位置的前面添加一个 APK Signing Block
区块,用于检验以上三个区块的完整性。
APK Signing Block
区块的构成是这样的
偏移 | 字节数 | 描述 |
---|---|---|
@+0 | 8 | 这个Block的长度(本字段的长度不计算在内) |
@+8 | n | 一组ID-value |
@-24 | 8 | 这个Block的长度(和第一个字段一样值) |
@-16 | 16 | 魔数 “APK Sig Block 42” |
区块2中 APK Signing Block
是由这几部分组成:2个用来标示这个区块长度的8字节 + 这个区块的魔数 + 这个区块所承载的数据(ID-value)。
其中 Android
是通过 ID-value
对中的 ID
为 0x7109871a
的 ID-value
进行校验,对对中的其它 ID-value
是不做检验处理的,那么我们可以向 ID-value
对中添加我们自己的 ID-value
,即渠道信息,这样使安装包可以在增加了渠道信息的情况下通过 Android
的安装包检验。
三、写入渠道信息
通过上面的分析我们得知,写入渠道信息需要修改安装包,这时候肯定会想到使用 gradle
插件对编译后的安装包文件进行修改。如下图所示,我们也可以看到, Walle
的源码目录中的plugin插件。
通过分析 plugin
的 gradle
依赖,我们知道这个插件的功能实现由 plugin
、 payload_writer
、 payload_reader
三个模块构成。我们先看实现了 org.gradle.api.Plugin<Project>
的 GradlePlugin
类。抛开异常检查和配置相关的代码,我们从主功能代码开始看。
@Override void apply(Project project) { ... applyExtension(project); applyTask(project); } void applyTask(Project project) { project.afterEvaluate { project.android.applicationVariants.all { BaseVariant variant -> ... ChannelMaker channelMaker = project.tasks.create("assemble${variantName}Channels", ChannelMaker); channelMaker.targetProject = project; channelMaker.variant = variant; channelMaker.setup(); channelMaker.dependsOn variant.assemble; } } } 复制代码
在gradle脚本运行时会调用实现了 org.gradle.api.Plugin<Project>
接口的类的 void apply(Project project)
方法,我们从该方法开始跟踪。这里主要调用了 applyTask(project)
。而 applyTask(project)
中创建了一个 ChannelMaker
的 gradle
任务对象,并把这个任务对象放在 assemble
任务(即完成了打包任务)后,可见 Walle
是通过 ChannelMaker
保存渠道信息的。接下来,我们便看 ChannelMaker
这个 groovy
文件。
@TaskAction public void packaging() { ... checkV2Signature(apkFile) ... if (targetProject.hasProperty(PROPERTY_CHANNEL_LIST)) { ... channelList.each { channel -> generateChannelApk(apkFile, channelOutputFolder, nameVariantMap, channel, extraInfo, null) } } else if (targetProject.hasProperty(PROPERTY_CONFIG_FILE)) { ... generateChannelApkByConfigFile(configFile, apkFile, channelOutputFolder, nameVariantMap) } else if (targetProject.hasProperty(PROPERTY_CHANNEL_FILE)) { ... generateChannelApkByChannelFile(channelFile, apkFile, channelOutputFolder, nameVariantMap) } else if (extension.configFile instanceof File) { ... generateChannelApkByConfigFile(extension.configFile, apkFile, channelOutputFolder, nameVariantMap) } else if (extension.channelFile instanceof File) { ... generateChannelApkByChannelFile(extension.channelFile, apkFile, channelOutputFolder, nameVariantMap) } } ... } 复制代码
在 ChannelMaker.groovy
的 packaging()
方法中,做了检验操作和一堆条件判断,最后都会调用以 generateChannel
为开头命名的方法。至于判断了什么,我们不要在意这些细节。这些名字以 generateChannel
开头的方法最后都会调用到 generateChannelApk()
,看代码:
def generateChannelApk(File apkFile, File channelOutputFolder, Map nameVariantMap, channel, extraInfo, alias) { ... ChannelWriter.put(channelApkFile, channel, extraInfo) ... } 复制代码
这个方法中比较关键的一段代码是 ChannelWriter.put(channelApkFile, channel, extraInfo)
即传入文件地址、渠道信息、 extra
信息后交由 ChannelWriter
完成写入工作。
ChannelWriter
封装在由 payload_writer
模块中,里面封装了方法调用。其中 void put(final File apkFile, final String channel, final Map<String, String> extraInfo)
间接调用了 void putRaw(final File apkFile, final String string, final boolean lowMemory)
:
public static void putRaw(final File apkFile, final String string, final boolean lowMemory) throws IOException, SignatureNotFoundException { PayloadWriter.put(apkFile, ApkUtil.APK_CHANNEL_BLOCK_ID, string, lowMemory); } 复制代码
这时调用进入了 PayloadWriter
类,渠道信息写入的关键代码便在这里面。这里从 void put(final File apkFile, final int id, final ByteBuffer buffer, final boolean lowMemory)
调用到 void putAll(final File apkFile, final Map<Integer, ByteBuffer> idValues, final boolean lowMemory)
:
public static void putAll(final File apkFile, final Map<Integer, ByteBuffer> idValues, final boolean lowMemory) throws IOException, SignatureNotFoundException { handleApkSigningBlock(apkFile, new ApkSigningBlockHandler() { @Override public ApkSigningBlock handle(final Map<Integer, ByteBuffer> originIdValues) { if (idValues != null && !idValues.isEmpty()) { originIdValues.putAll(idValues); } final ApkSigningBlock apkSigningBlock = new ApkSigningBlock(); final Set<Map.Entry<Integer, ByteBuffer>> entrySet = originIdValues.entrySet(); for (Map.Entry<Integer, ByteBuffer> entry : entrySet) { final ApkSigningPayload payload = new ApkSigningPayload(entry.getKey(), entry.getValue()); apkSigningBlock.addPayload(payload); } return apkSigningBlock; } }, lowMemory); } 复制代码
在 void putAll()
中调用了 handleApkSigningBlock()
,顾名思义,这个方法是处理 APK Signing Block
的,将渠道信息写入 Block
中。
static void handleApkSigningBlock(final File apkFile, final ApkSigningBlockHandler handler, final boolean lowMemory) throws IOException, SignatureNotFoundException { RandomAccessFile fIn = null; FileChannel fileChannel = null; try { // 由安装包路径构建一个RandomAccessFile对象,用于自由访问文件位置 fIn = new RandomAccessFile(apkFile, "rw"); // 获取fileChannel,通过fileChannel写文件 fileChannel = fIn.getChannel(); // 获取zip文件的comment长度 final long commentLength = ApkUtil.getCommentLength(fileChannel); // 找到Central Directory的初始偏移量 final long centralDirStartOffset = ApkUtil.findCentralDirStartOffset(fileChannel, commentLength); // 找到APK Signing Block final Pair<ByteBuffer, Long> apkSigningBlockAndOffset = ApkUtil.findApkSigningBlock(fileChannel, centralDirStartOffset); final ByteBuffer apkSigningBlock2 = apkSigningBlockAndOffset.getFirst(); final long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); // 找到APK Signature Scheme v2的ID-value final Map<Integer, ByteBuffer> originIdValues = ApkUtil.findIdValues(apkSigningBlock2); // 找到V2签名信息 final ByteBuffer apkSignatureSchemeV2Block = originIdValues.get(ApkUtil.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); // 校验签名信息是否存在 if (apkSignatureSchemeV2Block == null) { throw new IOException( "No APK Signature Scheme v2 block in APK Signing Block"); } final ApkSigningBlock apkSigningBlock = handler.handle(originIdValues); if (apkSigningBlockOffset != 0 && centralDirStartOffset != 0) { // read CentralDir fIn.seek(centralDirStartOffset); byte[] centralDirBytes = null; File tempCentralBytesFile = null; // read CentralDir ... centralDirBytes = new byte[(int) (fileChannel.size() - centralDirStartOffset)]; fIn.read(centralDirBytes); ... //update apk sign fileChannel.position(apkSigningBlockOffset); final long length = apkSigningBlock.writeApkSigningBlock(fIn); // update CentralDir ... // store CentralDir fIn.write(centralDirBytes); ... // update length fIn.setLength(fIn.getFilePointer()); // update CentralDir Offset // End of central directory record (EOCD) // Offset Bytes Description[23] // 0 4 End of central directory signature = 0x06054b50 // 4 2 Number of this disk // 6 2 Disk where central directory starts // 8 2 Number of central directory records on this disk // 10 2 Total number of central directory records // 12 4 Size of central directory (bytes) // 16 4 Offset of start of central directory, relative to start of archive // 20 2 Comment length (n) // 22 n Comment // 定位到EOCD中Offset of start of central directory,即central directory中央目录的超始位置 fIn.seek(fileChannel.size() - commentLength - 6); // 6 = 2(Comment length) + 4 (Offset of start of central directory, relative to start of archive) final ByteBuffer temp = ByteBuffer.allocate(4); temp.order(ByteOrder.LITTLE_ENDIAN); // 写入修改APK Signing Block之后的central directory中央目录的超始位置 temp.putInt((int) (centralDirStartOffset + length + 8 - (centralDirStartOffset - apkSigningBlockOffset))); // 8 = size of block in bytes (excluding this field) (uint64) temp.flip(); fIn.write(temp.array()); ... 复制代码
好了,写入渠道信息的代码大致上都在这里了,结合上面的代码和注释我们来做一下分析。上文我们提到,通过往 APK Signing Block
写入渠道信息完成多渠道打包,这里简要地说明一下流程。我们是这样从安装包中找到 APK Signing Block
的:
从 zip
结构中的 EOCD
出发,根据 EOCD
结构定位到 Offset of start of central directory(中央目录偏移量)
,通过中央目录偏移量找到中央目录的位置。因为 APK Signing Block
是在中央目录之前,所以我们可以从中央目录偏移量往前找到 APK Signing Block
的 size
,再通过 Offset of start of central directory(中央目录偏移量)
- size
来确定 APK Signing Block
的起始偏移量。这时候我们知道了 APK Signing Block
的位置,就可以拿到 ID-value
对去加入渠道信息,再将修改后的 APK Signing Block
和 Central Directory
同 EOCD
一起写入文件中。
这时候修改工作还没有完成,这里因为改动了 APK Signing Block
,所以在 APK Signing Block
后面的 Central Directory
起始偏移量也跟着改变了。这个起始偏移量是记录在 EOCD
中的,根据EOCD结构修改 Central Directory
的起始偏移量后写入工作就算完成了。
细心的朋友会发现,不是说 V2
签名会保护 EOCD
这一区块吗,修改了里面的超始偏移量还能通过校验吗?其实 Android
系统在使用 V2
校验安装包时,会把 EOCD
的 Central Directory
的起始偏移量换成 APK Signing Block
的偏移量再进行校验,所以修改 EOCD
中 Central Directory
的起始偏移量不会影响到校验。
四、读取渠道信息
在了解了 Walle
是如何写入渠道信息之后,去理解读取渠道信息就很简单了。 Walle
先拿到安装包文件,再根据 zip
文件结构找到 APK Signing Block
,从中读取出之前写入的渠道信息。具体的代码懒懒的笔者就不帖了。
五、总结
有一部分的 Coder
总是能做出创新性的东西,基于他们对于技术的理解做出更加方便、灵活的工具。在通过对 Walle
的分析中,我们可以学到,在清楚理解了 zip
结构、 Android
安装包检验原理,运行 gradle plugin
,就可以做出一款便于打包的工具。在这里分享美团多渠道打包工具 Walle
的原理实现,希望各位看了有所收获。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 多渠道打包
- Android 多渠道打包配置
- Walle —— Android多渠道打包神器
- App 多渠道打包及重签名方案
- Android美团多渠道打包Walle集成
- Android官方多渠道方案详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
架构真经
马丁L. 阿伯特(Martin L. Abbott)、迈克尔T.费舍尔(Michael T. Fisher) / 机械工业出版社 / 2017-4 / 79
前言 感谢你对本书第2版感兴趣!作为一本入门、进修和轻量级的参考手册,本书旨在帮助工程师、架构师和管理者研发及维护可扩展的互联网产品。本书给出了一系列规则,每个规则围绕着不同的主题展开讨论。大部分的规则聚焦在技术上,少数规则涉及一些关键的思维或流程问题,每个规则对构建可扩展的产品都是至关重要的。这些规则在深度和焦点上都有所不同。有些规则是高级的,例如定义一个可以应用于几乎任何可扩展性问题的模......一起来看看 《架构真经》 这本书的介绍吧!