android动态存放数组中,Android 动态写入信息到 APK

标签: 多渠道打包 , 动态写入APK , V2签名

如何实现快速多渠道打包?

如何将 Git 的 SHA-1 值、打包时间、友盟渠道等自定义信息写入到 APK 中?

这就需要我们今天要分享的技术了:动态写入信息到 apk。

一、核心干货

如果只用 V1 签名,放到 apk 的 META-INFO 目录即可。本篇讨论 V2 签名的情况。

在 V2 签名块中,签名信息是放在了 ID = 0x7109871a 的键值对块中。我们可以把其他自定义数据,也按照键值对块的格式,插入到签名块中。再修改 EoCDR

如果对 Zip 文件格式和 V2 签名块格式不了解,请移步到:《Android 端 V1/V2/V3 签名的原理》。这里只放一张 Zip 文件的结构图:

7a91c20c4b0d

Zip文件格式

二、具体实现

这篇文章会用 EoCDR 表示 Zip 文件的 End of Central Directory Record 区域。

接下来我们用 Kotlin 来实现「动态写入信息到apk」和「从apk中读取信息」:

1. 写入信息到 apk

先高屋建瓴地看一下写入时的详细步骤:

( 1). 获取注释长度;

( 2). 获取 EoCDR 的长度;

( 3). 得到 EoCDR 的偏移量;

( 4). 找到『保存了「中央目录区偏移量」的偏移量』位置A;

( 5). 读取A, 读取中央目录偏移量 centralDirOffset;

( 6). 根据 centralDirOffset, 读取和验证 v2 签名的魔数;

( 7). 验证通过后,获取两个『保存「签名块的大小」的位置』C1、C2 和签名块大小;

( 8). 将想要插入的数据,按照格式转为字节数组 bytes(长度必须是4096的整数倍);

( 9). 将 bytes 插入到 C2 之前;

(10). 更新 C1 和 C2 对应的值为 signBlockSize + bytes.size;

(11). 更新位置A的值为 centralDirOffset + bytes.size;

下面我们一步步来实现:

(1) 获取注释长度

在 Java 中,可以通过 ZipFile 获取注释的长度:

val file = File("Apk文件路径")

// 创建 ZipFile 文件,只用于读取注释长度

val zipFile = ZipFile(absolutePath)

val comment = zipFile.comment

val commentBytes = comment?.toByteArray()

val commentLength = commentBytes?.size ?: 0

(2) 获取 EoCDR 的长度

如果一个 Zip 文件没有注释,它的 EoCDR 长度为 22,所以 EoCDR 的真实长度就是 22 + 注释长度:

val eocdrLength = commentLength + 22

(3) 得到 EoCDR 的偏移量

EoCDR 的偏移量 = Apk文件长度 - EoCDR 的长度

val eocdrOffset = file.length() - eocdrLength

(4) 找到『保存了「中央目录区偏移量」的偏移量』位置

根据 EoCDR 的结构,「中央目录区的偏移量」保存在距离 EoCDR 开始位置 16 字节处:

val pointer = eocdrOffset + 16

(5) 读取「中央目录偏移量」

val centralDirectoryOffset = file.read(pointer, 4)

其中,read 方法是自定义的扩展方法,实现如下:

/**

* 从文件制定偏移位置开始,读取制定长度的二进制数据。

*

* @param offset 相对文件起始位置的偏移量

* @param length 读取的数据长度

*/

fun File.read(offset: Long, length: Int): ByteArray {

val inputStream = FileInputStream(this)

inputStream.skip(offset)

val buffer = ByteArray(length)

inputStream.read(buffer, 0, length)

inputStream.close()

return buffer

}

(6) 读取和验证 v2 签名的魔数

7a91c20c4b0d

V2签名数据块的结构.png

魔数保存在中央目录区前方,共16个字节,内容为 「APK Sig Block 42」:

val magicBytes = read(centralDirectoryOffset - 16, 16)

val magicString = magicBytes.toCharSequence()

if (magicString != "APK Sig Block 42") {

// 当前安装包不具有 V2 签名

}

(7) 获取两个『保存「签名块的大小」的位置』

根据签名块的结构,签名块大小保存在两个地方,一个在签名块的开始位置,一个在魔数前方,都是8个字节:

// 先获取结束位置的偏移量和大小

val signBlockSizeOffset = centralDirectoryOffset - 24

val signBlocksSize = file.read(signBlockSizeOffset, 8).toInt()

// 再获取开始位置的偏移量

val signBlockSizeOffsetStart = signBlockSizeOffset - signBlockSize + 16

(8) 将想要插入的数据转为字节数组 bytes

签名块中的数据都是按照 Size-ID-Value 的格式组织的,其中 Size 长8字节、ID 长4字节、Value 不定长,我们将这个组织过程封装为一个方法:

/**

* 用于构建一块符合签名块 Size-ID-Value 格式的 Byte 数组。

*/

fun build(id: Int, value: ByteArray): ByteArray {

val idBytes = id.toLittleEndianBytes()

val idValueSize = (4 + value.size).toLong()

val idValueSizeBytes = idValueSize.toLittleEndianBytes()

val bytes = ByteArray(8 + 4 + value.size)

System.arraycopy(idValueSizeBytes, 0, bytes, 0, 8)

System.arraycopy(idBytes, 0, bytes, 8, 4)

System.arraycopy(value, 0, bytes, 12, value.size)

return bytes

}

我们可以使用这个方法构建出一个可以插入到签名块中的 bytes 数组:

val customInfo = build(0x19920511, "要写入APK文件的自定义内容")

(9) 将自定义数据插入到签名块中

这就是个纯 Java 的问题,和本文关系不大,代码量较多但不难,这里略过具体实现,只给出函数定义:

/**

* 将数据value 插入文件的指定位置offset。

*/

fun File.insert(offset: Int, value: ByteArray) {

// 比较简单,具体实现略

}

调用这个函数,将自定义数据插入到 apk 文件:

file.insert(signBlockSizeOffset, customInfo)

(10) 更新签名块的大小

签名块的大小在两个地方保存了,我们需要把两个地方都修改了:

// 计算出新的大小

val newSize = signBlocksSize + customInfo.size

// 将大小转为字节数组(小端)

val newSizeBytes = newSize.toLittleEndianBytes()

// 修改签名块头部保存的大小

file.overwrite(signBlockSizeOffsetStart, newSizeBytes)

// 修改签名块尾部保存的大小

file.overwrite(signBlockSizeOffset + customInfo.size, newSizeBytes)

第 11 行的 + customInfo.size 是后来补上的,感谢评论中的 「依然菜刀」发现的问题👍 。

toLittleEndianBytes 和 overwrite 是扩展方法,代码量大但简单,这里给出 overwrite 的定义:

/**

* 将数据value,从文件的指定位置offset开始覆盖,长度为value.length。

*/

fun File.overwrite(offset: Int, value: ByteArray) {

// 比较简单,具体实现略

}

(11) 更新中央目录偏移量的位置

由于插入了新的数据,「中央目录的偏移量」会往后移,需要更新。同时,『保存「中央目录偏移量」的位置的偏移量』也会往后移,更新时需要找对地方:

// 新「中央目录偏移量」

val newOffset = centralDirectoryOffset + customInfo.size

// 将 int 型的值,转为字节数组

val newOffsetBytes = newOffset.toLittleEndianBytes()

// 新 『保存「中央目录偏移量」的位置的偏移量』

val newOffsetPointer = pointer + customInfo.size

// 更新文件

file.overwrite(newOffsetAddress, newOffsetBytes)

根据上面11个步骤,我们就能将自定义的信息写入到 apk 中,并且能通过 Android 系统的签名校验。

2. 从 APK 中读取信息

根据写入的步骤,很容易知道读取时需要怎么做,读取时的详细步骤:

(1). 获取注释长度;(和写入相同)

(2). 获取 EoCDR 的长度;(和写入相同)

(3). 得到 EoCDR 的偏移量;(和写入相同)

(4). 找到『保存了「中央目录区偏移量」的偏移量』位置A;(和写入相同)

(5). 读取A, 读取中央目录偏移量 centralDirOffset;(和写入相同)

(6). 根据 centralDirOffset, 读取和验证 v2 签名的魔数;(和写入相同)

(7). 验证通过后,获取两个『保存「签名块的大小」的位置』C1、C2 和签名块大小;(和写入相同)

(8). 遍历每一个 Size-ID-Value 块,直到找到想要的数据块。

可以看出,前面7个步骤都和写入时相同,我们看看最后一个步骤怎么实现:

(8) 遍历签名块找到指定的ID

从第(7)步我们拿到了签名块的开始位置 signBlockSizeOffsetStart 和 结束位置 signBlockSizeOffset,这一步只需遍历即可:

val signBlockSizeOffsetStart = ... // 见第(7)步

val signBlockSizeOffset = ... // 见第(7)步

val dstID = ... // 当初写入时用的ID

val dstSize: Int? = null

val dstValueBytes: ByteArray? = null

var offset = signBlockSizeOffsetStart + 8

while (true) {

// 先读取当前 ID-Value 块的 Size,长度8字节

val tempSize = read(offset, 8).toInt()

// 再读取 ID,长度4字节

val tempID = read(offset + 8, 4).toInt()

// 找到了退出循环

if (tempID == dstID) {

dstSize = tempSize

dstValueBytes = read(offset + 12, tempSize - 4)

break

}

// 找不到继续

offset += tempSize + 8

if (offset >= signBlockSizeOffset) {

// log { "break; offset = $offset, tailOffset = $tailOffset" }

break

}

}

// 解析 value,这里按照字符串解析

val value = dstValueBytes.toString()

这样我们就能从 apk 中读取出我们写入的数据了~

有了该技术,我们可以真的做到每一个 apk 包含的信息都不相同。

例如,小明将分享链接给到小红,小红下载apk安装打开后,自动弹出「您是小明邀请的用户,具有....特权」等等千人千面的功能。

即 End of Central Directory Record,Zip文件末尾记录中央目录区偏移量和Zip其他信息的区域。 ↩

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值