1#apk文件结构

apk文件本质来说就是zip,zip的文件格式就是:
[文件头+文件数据+数据描述符]{此处可重复n次}+核心目录+目录结束标识
当压缩包中有多个文件时,就会有多个[文件头+文件数据+数据描述符]
zip可以分为3个数据区:

压缩源文件数据区,Central directory 核心目录, 目录结束标识

1)压缩源文件数据区:

记录着压缩的所有文件的内容信息,每个压缩文件都由local file header 、file data、data descriptor三部分组成,在这个数据区中每一个压缩的源文件/目录都是一条记录。

local file header 文件头

用于标识该文件的开始,记录了该压缩文件的信息。文件头标识,值固定(0x04034b50)
v2多渠道打包原理&多渠道增量更新方案 - 图1

2)Central directory 核心目录

记录了压缩文件的目录信息,在这个数据区中每一条纪录对应在压缩源文件数据区中的一条数据。文件标示,值固定(0x02014b50)
v2多渠道打包原理&多渠道增量更新方案 - 图2

3)目录结束标识

目录结束标识存在于整个归档包的结尾,用于标记压缩的目录数据的结束。每个压缩文件必须有且只有一个EOCD记录。标记头(0x06054b50)
主要看这个结构的注释。

Offset Bytes Description
0 4 End of central directory signature = 0x06054b50 核心目录结束标记(0x06054b50)
4 2 Number of this disk 当前磁盘编号
6 2 number of the disk with the start of the central directory 核心目录开始位置的磁盘编号
8 2 total number of entries in the central directory on this disk 该磁盘上所记录的核心目录数量
10 2 total number of entries in the central directory 核心目录结构总数
12 4 Size of central directory (bytes) 核心目录的大小
16 4 offset of start of central directory with respect to the starting disk number 核心目录开始位置相对于archive开始的位移
20 2 .ZIP file comment length(n) 注释长度
22 n .ZIP Comment 注释内容

1.zip注释可随意修改(最大长度2个字节即256长度)
2.如何寻找注释内容?找标记头位移20位
如:看某个apk注释
v2多渠道打包原理&多渠道增量更新方案 - 图3

2#新的应用签名方案APK Signature Scheme v2

  1. android {
  2. ...
  3. defaultConfig { ... }
  4. signingConfigs {
  5. release {
  6. ...
  7. v2SigningEnabled true
  8. }
  9. }
  10. }

新的签名方案会在ZIP文件格式的 Central Directory 区块所在文件位置的前面添加一个APK Signing Block区块,如下
604a4dc5.png
新应用签名方案的签名信息会被保存在区块2(APK Signing Block)中, 而区块1(Contents of ZIP entries)、区块3(ZIP Central Directory)、区块4(ZIP End of Central Directory)是受保护的,在签名后任何对区块1、3、4的修改都逃不过新的应用签名方案的检查。新的签名方案会对所有文件的修改做遍历签名,所以传统的多渠道方式如META-INF加渠道文件或者往APK中添加ZIP Comment都会失效!
**

区块2(APK Signing Block)的格式

偏移 字节数 描述
@+0 8 这个Block的长度(本字段的长度不计算在内)
@+8 n 一组ID-value
@-24 8 这个Block的长度(和第一个字段一样值)
@-16 16 魔数 “APK Sig Block 42”

ID-value,它由一个8字节的长度标示+4字节的ID+它的负载组成。V2的签名信息是以ID(0x7109871a)的ID-value来保存在这个区块中,而且它是可以有若干个这样的ID-value来组成,那么我们是不是可以把渠道信息当作一组ID-value写入呢?我们先来看v2的验证签名过程:
18666479.png
#android.util.apk.ApkSignatureSchemeV2Verifier
并且安卓的源码只对ID(0x7109871a)的ID-value做处理,其余的ID-value不做处理,如下:

  1. public static ByteBuffer findApkSignatureSchemeV2Block(
  2. ByteBuffer apkSigningBlock,
  3. Result result) throws SignatureNotFoundException {
  4. checkByteOrderLittleEndian(apkSigningBlock);
  5. // FORMAT:
  6. // OFFSET DATA TYPE DESCRIPTION
  7. // * @+0 bytes uint64: size in bytes (excluding this field)
  8. // * @+8 bytes pairs
  9. // * @-24 bytes uint64: size in bytes (same as the one above)
  10. // * @-16 bytes uint128: magic
  11. ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
  12. int entryCount = 0;
  13. while (pairs.hasRemaining()) {
  14. entryCount++;
  15. if (pairs.remaining() < 8) {
  16. throw new SignatureNotFoundException(
  17. "Insufficient data to read size of APK Signing Block entry #" + entryCount);
  18. }
  19. long lenLong = pairs.getLong();
  20. if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
  21. throw new SignatureNotFoundException(
  22. "APK Signing Block entry #" + entryCount
  23. + " size out of range: " + lenLong);
  24. }
  25. int len = (int) lenLong;
  26. int nextEntryPos = pairs.position() + len;
  27. if (len > pairs.remaining()) {
  28. throw new SignatureNotFoundException(
  29. "APK Signing Block entry #" + entryCount + " size out of range: " + len
  30. + ", available: " + pairs.remaining());
  31. }
  32. int id = pairs.getInt();
  33. if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
  34. return getByteBuffer(pairs, len - 4);
  35. }
  36. result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
  37. pairs.position(nextEntryPos);
  38. }
  39. throw new SignatureNotFoundException(
  40. "No APK Signature Scheme v2 block in APK Signing Block");
  41. }

所以,我们只需要把渠道信息当作一组ID-value写在ID(0x7109871a)的ID-value的前面,步骤:
1、根据标识(0x06054b50)找到EOCD位移。
2、EOCD起始位移16字节,找到记录中央目录的起始位移。
3、根据插入ID-value的大小修改EOCD中记录中央目录的位移。
4、根据中央目录起始位移-24找到记录签名块大小。
5、修改前后记录签名块大小的值。

  1. private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
  2. private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
  3. private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
  4. private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
  5. private final static int CHANNEL_FLAG = 0x12345678; //渠道id标识
  6. private static void insertChannelId(RandomAccessFile apk,int adChannelId) {
  7. try{
  8. byte[] channelIdBuff = intToBytes2(adChannelId);
  9. int contentSize = channelIdBuff.length;
  10. //根据标识(0x06054b50)找到EOCD
  11. Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
  12. ByteBuffer eocd = eocdAndOffsetInFile.first;
  13. long eocdOffset = eocdAndOffsetInFile.second;
  14. if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
  15. throw new SignatureNotFoundException("ZIP64 APK not supported");
  16. }
  17. int size = 8 + 4 + contentSize;
  18. long neweocdOffset = eocdOffset + size;
  19. //查找中央目录位移
  20. long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
  21. long newCentralDirOffset = centralDirOffset + size;
  22. //查找签名块位移
  23. Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile = findApkSigningBlock(apk, centralDirOffset);
  24. long newSigningBlockSize = apkSigningBlockAndOffsetInFile.first.capacity()-8 + size;
  25. //插入一组渠道 格式为[大小:标识:内容]
  26. int pos = (int) (apkSigningBlockAndOffsetInFile.second + 8);
  27. File tmp = File.createTempFile("tmp", null);//创建一个临时文件存放数据;
  28. FileInputStream fis = new FileInputStream(tmp);
  29. FileOutputStream fos = new FileOutputStream(tmp);
  30. apk.seek(pos);//把指针移动到指定位置
  31. byte[] buf = new byte[1024];
  32. int len = -1;
  33. //把指定位置之后的数据写入到临时文件
  34. while((len = apk.read(buf)) != -1){
  35. fos.write(buf, 0, len);
  36. }
  37. apk.seek(pos);//再把指针移动到指定位置,插入追加的数据
  38. ByteBuffer buffer = ByteBuffer.allocate(size);
  39. buffer.order(ByteOrder.LITTLE_ENDIAN);
  40. buffer.putLong(size-8); //大小
  41. buffer.putInt(CHANNEL_FLAG); //标识
  42. buffer.putInt(adChannelId); //内容
  43. apk.write(buffer.array());
  44. //再把临时文件的数据写回
  45. while((len = fis.read(buf)) > 0){
  46. apk.write(buf, 0, len);
  47. }
  48. apk.seek(neweocdOffset+ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
  49. buffer = ByteBuffer.allocate(4);
  50. buffer.order(ByteOrder.LITTLE_ENDIAN);
  51. buffer.clear();
  52. buffer.putInt((int) newCentralDirOffset);
  53. apk.write(buffer.array());//修改eocd中央目录位移
  54. apk.seek(apkSigningBlockAndOffsetInFile.second);//移到签名块头
  55. buffer = ByteBuffer.allocate(8);
  56. buffer.order(ByteOrder.LITTLE_ENDIAN);
  57. buffer.clear();
  58. buffer.putLong(newSigningBlockSize);
  59. apk.write(buffer.array()); //修改签名头大小
  60. apk.seek(newCentralDirOffset-24);
  61. buffer.clear();
  62. buffer.putLong(newSigningBlockSize);
  63. apk.write(buffer.array()); //修改签名尾大小
  64. } catch (Exception e) {
  65. e.printStackTrace();
  66. }
  67. }

3#多渠道增量更新

应用启动时将手机端的Version Code和应用APK文件的MD5值发送到服务器端。服务器通过对MD5值查找到老版本的APK, 同新老版本的APK做diff, 生成patch文件,返回给SDK。 SDK再将patch文件和手机上的老版本APK文件合成生成新版本的APK。手机端生成的新版APK文件的MD5值会和服务器端的新版APK MD5值保持严格一致。在此过程中,服务器必须存在新老两个版本的APK文件