Android V2签名与校验原理分析

【前言】

     V1签名作为一种历史悠久的签名方式,弊端也是比较明显的,一方面由于V1签名是对Apk内的单个文件逐一计算摘要进行签名校验的,所以要是Apk内的文件比较多,计算速度是非常慢的,同时又因为只对单个文件的完整性进行校验,那么对apk压缩包包体进行篡改的话,签名依然还是可以校验通过,完整性的校验工作做得不够到位。到了Android 7.0V2签名方式就应运而生,V2签名一种全文件签名方案,它对压缩包的三大基本组成部分:数据区中央目录记录区中央目录记录结尾区进行分块,每小块 1MB,然后并行计算出每小块的摘要值,最后将计算出来的摘要值拼接起来再一起算出整体的摘要值,并用私钥对其进行签名,计算速度方面相比V1签名有很大的提升,同时完整性校验方面做得更加周全。

一、V2签名过程分析

1、V2签名Apk的结构示意图

在这里插入图片描述

     使用 APK v2签名方案 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于ZIP 中央目录部分之前并紧邻该部分。在APK 签名分块内,v2 签名签名者身份信息会存储在 APK 签名方案 v2 分块中。APK v2签名方案 是在 Android 7.0 (Nougat) 才引入的,为了使 APK 可在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APK 进行签名,然后再使用 v2 方案对其进行签名。
     为了保持与 v1 APK 格式向后兼容,v2 及更高版本的 APK 签名会存储在“APK 签名分块”内,该分块是为了支持 APK 签名方案 v2 而引入的一个新容器。在 APK 文件中,“APK 签名分块”位于“ZIP 中央目录”之前并紧邻该部分。该分块包含多个“ID-VALUE”对,所采用的封装方式有助于更轻松地在 APK 中找到该分块,APK 的 v2 签名会存储为一个“ID-VALUE”对,其中 ID 为 0x7109871a

2、APK签名分块(APK Signing Block)格式

在这里插入图片描述

     APK签名分块的前8个字节记录了APK签名分块的大小 size of block(不含自身8字节),其后紧接着键值对数据块,数据块由一个个的键值对块组成。 每个键值对块的开始8字节记录了「键值对的ID」+「键值对的Value」的大小,接下来4字节键值对的ID,后面紧跟着对应的值。 ID = 0x7109871a 的键值对块就是保存V2签名信息的地方。 键值对数据块的后面还有8个字节,也是用于记录「整个APK签名分块」的大小,它的值和最开始的8字节相同。 签名块的末尾是一个魔数magic,也就是APK Sig Block 42的 ASCII 码(小端排序)。
     在解析 APK 时,首先要通过以下方法找到“ZIP 中央目录”的起始位置:在文件末尾找到“ZIP 中央目录结尾”记录,然后从该记录中读取“中央目录”起始偏移量。通过 magic 值,可以快速确定“中央目录”前方可能是“APK 签名分块”。然后,通过 size of block 值,可以高效地找到该分块在文件中的起始位置,在解译该分块时,应忽略 ID 未知的“ID-值”对
【注意】
      由上图分析可知,APK签名分块的大小 size of block占8个字节,表示的是除了上图红框之外的所有数据的大小(即黄色框里面那部分数据的大小),一开始以为 size of block取值范围:0~ 263-1 , 结果看了验证V2签名源码,发现size of block取值范围为:24 ~ (整型最大值-8),即24 ~ (231-1-8)
      构造APK签名区块的代码逻辑如下:

    private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
    public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
    private static final byte[] APK_SIGNING_BLOCK_MAGIC =
          new byte[] {
              0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
              0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
          };
    public static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
  /**
     * 生成签名区块数据
     * @param apkSignatureSchemeBlockPairs
     * @return
     */
    public static byte[] generateApkSigningBlock(
            List<Pair<byte[], Integer>> apkSignatureSchemeBlockPairs) {
        // FORMAT:
        // uint64:  size (excluding this field)
        // repeated ID-value pairs:
        //     uint64:           size (excluding this field)
        //     uint32:           ID
        //     (size - 4) bytes: value
        // (extra verity ID-value for padding to make block size a multiple of 4096 bytes)
        // uint64:  size (same as the one above)
        // uint128: magic

        int blocksSize = 0;
        for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
            blocksSize += 8 + 4 + schemeBlockPair.getFirst().length; // size + id + value
        }

        int resultSize =
                8 // size
                + blocksSize
                + 8 // size
                + 16 // magic
                ;
        ByteBuffer paddingPair = null;
        //若是大小不是4096的倍数,那么需要增加填充块,填充块没有value
        if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
            int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
                    (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
            if (padding < 12) {  // minimum size of an ID-value pair,键值对最小也得8+4
                padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
            }
            paddingPair = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN);
            //填充块键值对的大小
            paddingPair.putLong(padding - 8);
            //ID
            paddingPair.putInt(VERITY_PADDING_BLOCK_ID);
            paddingPair.rewind();
            resultSize += padding;
        }

        ByteBuffer result = ByteBuffer.allocate(resultSize);
        result.order(ByteOrder.LITTLE_ENDIAN);

        //除了当前记录大小的8字节之外的剩余字节大小
        long blockSizeFieldValue = resultSize - 8L;
        result.putLong(blockSizeFieldValue);

        for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
            byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst();
            int apkSignatureSchemeId = schemeBlockPair.getSecond();
            long pairSizeFieldValue = 4L + apkSignatureSchemeBlock.length;
            // ID -Value键值端的大小
            result.putLong(pairSizeFieldValue);
            // 4字节的ID,比如:v2签名ID: APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a
            result.putInt(apkSignatureSchemeId);
            // Value数据
            result.put(apkSignatureSchemeBlock);
        }

        if (paddingPair != null) {
            result.put(paddingPair);
        }

        //倒数第24字节开始的8个字节,也是写入签名区块的大小
        result.putLong(blockSizeFieldValue);
        // 16字节:APK Sig Block 42的 ASCII 码
        result.put(APK_SIGNING_BLOCK_MAGIC);

        return result.array();
    }

3、V2签名信息数据格式

在这里插入图片描述
     APK 由一个或多个签名者签名,每个签名者均由一个签名密钥来表示。该信息会以“APK 签名方案 v2 分块”的形式存储。对于每个签名者,都会存储以下信息:

  • (签名算法、摘要、签名)元组。摘要会存储起来,以便将签名验证和 APK 内容完整性检查拆开进行。
  • 表示签名者身份的 X.509 证书链。
  • 采用键值对形式的其他属性。

在这里插入图片描述

     对于每位签名者,都会使用收到的列表中支持的签名来验证 APK。签名算法未知的签名会被忽略。如果遇到多个支持的签名,则由每个实现来选择使用哪个签名。这样一来,以后便能够以向后兼容的方式引入安全系数更高的签名方法。建议的方法是验证安全系数最高的签名。

4、APK摘要计算过程

为了保护 APK 内容,APK 包含以下 4 个部分:

  • ZIP 条目的内容(从偏移量 0 处开始一直到“APK 签名分块”的起始位置)
  • APK 签名分块
  • ZIP 中央目录
  • ZIP 中央目录结尾

在这里插入图片描述
APK 签名方案 v2 负责保护第 1、3、4 部分的完整性,以及第 2 部分包含的“APK 签名方案 v2 分块”中的 signed data 分块的完整性第 1、3 和 4 部分的完整性通过其内容的一个或多个摘要来保护,这些摘要存储在 signed data 分块中,而这些signed data分块则通过一个或多个签名来保护
在这里插入图片描述
第 1、3 和 4 部分的摘要采用以下计算方式,类似于两级 Merkle 树:

① 拆分块chunk

     将每个部分(即上面标注第1、3、4部分)拆分成多个大小为 1 MB大小的块chunk,最后一个块chunk可能小于1MB。之所以分块,是为了可以通过并行计算摘要以加快计算速度;

② 计算块chunk摘要

     字节 0xa5 + 块的长度(字节数) + 块的内容 拼接起来用对应的摘要算法进行计算出每一块的摘要值;

③ 计算整体摘要

     字节 0x5a + chunk数 + 块的摘要(按块在 APK 中的顺序)拼接起来用对应的摘要算法进行计算出整体的摘要值;

【注意】
     中央目录结尾记录中包含了中央目录的起始偏移量,插入APK签名分块后,中央目录的起始偏移量将发生变化。故在校验签名计算摘要时,需要把中央目录的起始偏移量当作APK签名分块的起始偏移量

二、V2签名验证过程分析

     因为V2签名机制是在Android 7.0中引入的,为了使APK可在Android 7.0以下版本中安装,应先用V1签名对APK进行签名,再用V2方案进行签名。要注意顺序一定是先V1签名再V2签名,因为V1签名的改动会修改到ZIP三大部分的内容,先使用V2签名再V1签名会破坏V2签名的完整性。
     在 Android 7.0 以上版本,会优先以 v2方案验证 APK,在Android 7.0以下版本中,系统会忽略 v2 签名,仅验证 v1 签名。Android 7.0+的校验过程如下:

在这里插入图片描述

1、防回滚保护

     因为在经过V2签名的APK中同时带有V1签名,攻击者可能将APK的V2签名删除,使得Android系统只校验V1签名。为了防范此类攻击,带 v2 签名的 APK 如果还带 V1 签名,其 META-INF/*.SF 文件的主要部分中必须包含 X-Android-APK-Signed 属性。该属性的值是一组以英文逗号分隔APK 签名方案 ID(v2 方案的 ID 为 2)。在验证 v1 签名时,对于此组中验证程序首选的 APK 签名方案(例如,v2 方案),如果 APK 没有相应的签名,APK 验证程序必须要拒绝这些 APK。此项保护依赖于 META-INF/*.SF 文件受 v1 签名保护这一事实。
在这里插入图片描述

     攻击者可能还会试图从“APK 签名方案 v2 分块”中删除安全系数较高的签名。为了防范此类攻击,对 APK 进行签名时使用的签名算法 ID 的列表存储在通过各个签名保护的 signed data 分块中。

2、签名校验过程

     我们知道跟安装包相关的处理逻辑都会经过PackageManagerService,下面时序图展示了从PackageManagerService开始,一直到V2签名校验函数的流程,在Android Studio中下载对应版本SDK的源码,双击Shift键,输入搜索PackageManagerService即可一步步找到V2签名校验的源码,以下是android api 30的源码时序图:
在这里插入图片描述
下面来看看怎么从apk中找到APK签名分块

   /**
     * 查找APK签名分块
     * @param apk apk文件
     * @param centralDirOffset 中央目录开始位置的偏移量
     * @return
     * @throws IOException
     * @throws SignatureNotFoundException
     */
    static Pair<ByteBuffer, Long> findApkSigningBlock(
            RandomAccessFile apk, long centralDirOffset)
                    throws IOException, SignatureNotFoundException {
        // FORMAT:
        // OFFSET       DATA TYPE  DESCRIPTION
        // * @+0  bytes uint64:    size in bytes (excluding this field)
        // * @+8  bytes payload
        // * @-24 bytes uint64:    size in bytes (same as the one above)
        // * @-16 bytes uint128:   magic

        //中央目录的开始位置偏移小于32,抛异常,因为 APK签名分块不算上键值对的大小,就至少32字节(8字节表示区块大小+8字节表示区块大小+16字节魔数)了
        if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
            throw new SignatureNotFoundException(
                    "APK too small for APK Signing Block. ZIP Central Directory offset: "
                            + centralDirOffset);
        }
        // Read the magic and offset in file from the footer section of the block:
        // * uint64:   size of block
        // * 16 bytes: magic
        ByteBuffer footer = ByteBuffer.allocate(24);
        footer.order(ByteOrder.LITTLE_ENDIAN);
        //指针指向中央目录开始位置往前移动24个字节的位置
        apk.seek(centralDirOffset - footer.capacity());
        //从指针位置开始读取24个字节的数据放进footer中
        apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity());
        //下面主要比较是否为等于“APK Sig Block 42”小端排序的值
        //footer.getLong(8):从第8个字节开始读取8个字节
        if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
                //footer.getLong(16) : 从第16个字节开始读取8个字节
                || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
            throw new SignatureNotFoundException(
                    "No APK Signing Block before ZIP Central Directory");
        }
        // Apk签名分块尾部记录的分块大小
        long apkSigBlockSizeInFooter = footer.getLong(0);
        // 大小 < 24 或者大于 整型最大值-8,抛异常
        if ((apkSigBlockSizeInFooter < footer.capacity())
                || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
            throw new SignatureNotFoundException(
                    "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
        }
        // Apk签名分块的总大小
        int totalSize = (int) (apkSigBlockSizeInFooter + 8);
        // Apk签名分块开始位置的偏移量 = 中央目录开始位置偏移量- Apk签名分块的总大小 
        long apkSigBlockOffset = centralDirOffset - totalSize;
        if (apkSigBlockOffset < 0) {
            throw new SignatureNotFoundException(
                    "APK Signing Block offset out of range: " + apkSigBlockOffset);
        }
        ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
        apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
        //指针指向APK签名分块开始位置
        apk.seek(apkSigBlockOffset);
        //从指针位置开始读取totalSize个字节的数据存到apkSigBlock中
        apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity());
        //从第0字节开始读取8个字节,就是记录在APK签名分块开头的“Apk签名分块的总大小”
        long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
        //判断开头跟结尾记录的总大小是否相等
        if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
            throw new SignatureNotFoundException(
                    "APK Signing Block sizes in header and footer do not match: "
                            + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
        }
 
        return Pair.create(apkSigBlock, apkSigBlockOffset);
    }

、先从ZIP中央目录开始位置centralDirOffset,指针往前16个字节,然后读取16个字节数据,判断是否等于魔数“APK Sig Block 42”ASCII码值
、再从ZIP中央目录开始位置centralDirOffset,指针往前24个字节,然后读取8个字节的数据,这个值就是尾部记录的APK签名分块大小apkSigBlockSizeInFooter
尾部记录的APK签名分块大小apkSigBlockSizeInFooter + 8字节,就是APK签名分块整体的大小totalSizeAPK签名分块开始位置apkSigBlockOffset = ZIP中央目录开始位置centralDirOffset- APK签名分块整体的大小totalSize
、从APK签名分块开始位置apkSigBlockOffset开始,读取8个字节数据,这个值就是头部记录的APK签名分块大小apkSigBlockSizeInHeader
、假如 头部记录的APK签名分块大小apkSigBlockSizeInHeader = 尾部记录的APK签名分块大小apkSigBlockSizeInFooter,那么从APK签名分块开始位置apkSigBlockOffset 开始,读取APK签名分块整体的大小totalSize个字节数据,这就是整个APK签名分块数据
在这里插入图片描述
接下来看看怎么从APK签名分块中找到V2签名信息

    /**
     * 从APK签名分块中找到blockId指定的键值
     * @param apkSigningBlock 签名分块数据
     * @param blockId 分块键id
     * @return
     * @throws SignatureNotFoundException
     */
    static ByteBuffer findApkSignatureSchemeBlock(ByteBuffer apkSigningBlock, int blockId)
            throws SignatureNotFoundException {
        checkByteOrderLittleEndian(apkSigningBlock);
        // FORMAT:
        // OFFSET       DATA TYPE  DESCRIPTION
        // * @+0  bytes uint64:    size in bytes (excluding this field)
        // * @+8  bytes pairs
        // * @-24 bytes uint64:    size in bytes (same as the one above)
        // * @-16 bytes uint128:   magic

        //从Apk签名分块的第8字节开始读到APK签名分块的倒数24字节,这一块也刚好是键值对数据区
        ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);

        int entryCount = 0;
        while (pairs.hasRemaining()) {
            entryCount++;
            //因为表示键值对的长度是8个字节,小于8字节是有问题的
            if (pairs.remaining() < 8) {
                throw new SignatureNotFoundException(
                        "Insufficient data to read size of APK Signing Block entry #" + entryCount);
            }
            //读取当前键值对的长度,因为8字节,所以使用getLong读取,每次get之后,指针都会往前移动一定字节
            long lenLong = pairs.getLong();
            //因为键ID的长度用4个字节表示,小于4个字节有问题,同时键值对的长度设置不超过整型的最大值
            if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount
                                + " size out of range: " + lenLong);
            }
            int len = (int) lenLong;
            //下一个键值对开始位置,就是当前键值对开始位置(当前键值对长度值之后) + 当前键值对长度
            int nextEntryPos = pairs.position() + len;
            //要是记录的键值对长度超过剩余的数据长度也是有问题
            if (len > pairs.remaining()) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount + " size out of range: " + len
                                + ", available: " + pairs.remaining());
            }
            //读取当前键值对的键ID, 因为是4个字节,所以用getInt读取
            int id = pairs.getInt();
            //假如键跟传进来的一致,那么返回值的数据,值的长度 = 键值对的长度-键ID的长度 
            if (id == blockId) {
                return getByteBuffer(pairs, len - 4);
            }
            //指针指向下一个键值对开始位置
            pairs.position(nextEntryPos);
        }

        //最后都没有找到指定ID的键值对,那么抛异常
        throw new SignatureNotFoundException(
                "No block with ID " + blockId + " in APK Signing Block.");
    }

键值对数据分块是保存着一个个带有长度前缀的键值对,大致如下:
在这里插入图片描述
其中,键值对长度 = key的长度(固定4个字节) + value的长度,即键值对长度不含自身的长度(固定8字节)
ByteBuffer 读取数据时候,读4个字节的数据可以用getInt,读8个字节的数据可以用getLong,使用这两个方法之后,指针会自动往前移动对应的字节
在这里插入图片描述
最后来看看V2签名信息校验流程:
、先从V2签名信息区中读取被签名的数据signedData多个签名者的签名signatures公钥字节数据publicKeyBytes
、从多个签名者的签名signatures中找出安全系数最高的签名算法bestSigAlgorithm以及该算法对应的签名bestSigAlgorithmSignatureBytes
、用公钥字节数据publicKeyBytes构造出公钥publicKey,然后使用公钥publicKey签名bestSigAlgorithmSignatureBytes进行解密得到被签名的数据signedData的hash值H1,然后对被签名的数据signedData计算得到hash值H2, 要是H1 = H2, 那么签名验证通过
、然后读出安全系数最高的签名算法bestSigAlgorithm对应的APK摘要值contentDigest
、接着读取出签名用到的证书certificates,并从第一个证书中读取出公钥字节数据certificatePublicKeyBytes,要是公钥字节数据certificatePublicKeyBytes = 公钥字节数据publicKeyBytes,那么公钥验证通过
、开始计算对压缩包三大组成部分:ZIP条目内容ZIP中央目录ZIP中央目录尾部,分别分成1MB的大小(每部分最后一块可能不足1MB), 然后计算出摘要值(注意:计算摘要之前,ZIP中央目录尾部记录的ZIP中央目录开始位置偏移量要修改成APK签名分块开始位置的偏移量,因为给APK进行V2签名时候,就是没有算上加入APK签名分块

【扩展知识】

三、APK摘要计算代码片段(关键部分加了注释)

 /**
     * 计算每一个1MB数据块的摘要值以及所有数据块摘要整体串联起来的摘要值
     * @param executor 并行计算
     * @param digestAlgorithms 摘要算法集合
     * @param contents 包括zip条目内容、zip中央目录、 zip中央目录结尾 3部分的数据
     * @param outputContentDigests 保存摘要算法与APK整体摘要值
     * @throws NoSuchAlgorithmException
     * @throws DigestException
     */
    static void computeOneMbChunkContentDigests(
            RunnablesExecutor executor,
            Set<ContentDigestAlgorithm> digestAlgorithms,
            DataSource[] contents,
            Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
            throws NoSuchAlgorithmException, DigestException {

        long chunkCountLong = 0;

        for (DataSource input : contents) {
            //计算每一部分按每块1MB分割,可以分割的块数
            chunkCountLong += getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
        }

        if (chunkCountLong > Integer.MAX_VALUE) {
            throw new DigestException("Input too long: " + chunkCountLong + " chunks");
        }

        //记录分割为每块为1MB之后,总共的块数
        int chunkCount = (int) chunkCountLong;

        List<ChunkDigests> chunkDigestsList = new ArrayList<>(digestAlgorithms.size());
        for (ContentDigestAlgorithm algorithms : digestAlgorithms) {
            //添加每个摘要算法对应的前5个字节的数据 {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}.
            chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount));
        }

        //按顺序提供切割成1MB的数据块的提供商
        ChunkSupplier chunkSupplier = new ChunkSupplier(contents);
        //多线程并行计算切分出来数据块的摘要值
        executor.execute(new RunnablesProvider() {
            @Override
            public Runnable createRunnable() {
                return new ChunkDigester(chunkSupplier, chunkDigestsList);
            }
        });

        // Compute and write out final digest for each algorithm.
        // 对于每个摘要算法,算出整体的摘要签名
        for (ChunkDigests chunkDigests : chunkDigestsList) {
            MessageDigest messageDigest = chunkDigests.createMessageDigest();
            outputContentDigests.put(
                    chunkDigests.algorithm,
                    messageDigest.digest(chunkDigests.concatOfDigestsOfChunks));
        }
    }

ChunkDigests 这个类主要用记录拼接进来的每一块的摘要值

    private static class ChunkDigests {
        private final ContentDigestAlgorithm algorithm;
        private final int digestOutputSize;
        private final byte[] concatOfDigestsOfChunks;

        private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) {
            this.algorithm = algorithm;
            digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes();
            concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize];

            // Fill the initial values of the concatenated digests of chunks, which is
            // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}.
            concatOfDigestsOfChunks[0] = 0x5a;
            //将块数chunkCount设置到concatOfDigestsOfChunks字节数组后4字节中
            setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1);
        }

        private MessageDigest createMessageDigest() throws NoSuchAlgorithmException {
            return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm());
        }

        private int getOffset(int chunkIndex) {
            return 1 + 4 + chunkIndex * digestOutputSize;
        }
    }
    
   private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
        result[offset] = (byte) (value & 0xff);
        result[offset + 1] = (byte) ((value >> 8) & 0xff);
        result[offset + 2] = (byte) ((value >> 16) & 0xff);
        result[offset + 3] = (byte) ((value >> 24) & 0xff);
    }

其中,对每一部分按1MB分割(最后一块可能不足1MB),计算可以分割的块数, 实现逻辑如下:

    /**
     * 计算按每块大小为:chunksize,可以分割成多少块
     * @param inputSize
     * @param chunkSize
     * @return
     */
    private static long getChunkCount(long inputSize, long chunkSize) {
        return (inputSize + chunkSize - 1) / chunkSize;
    }

ChunkSupplier这个类是用来构造返回每一块的数据,可以多线程进行操作

  /**
     * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a
     * supplied {@link DataSource}, the data from the next {@link DataSource}
     * are NOT concatenated. Only the next call to get() will fetch from the
     * next {@link DataSource} in the input {@link DataSource} array.
     */
    private static class ChunkSupplier implements Supplier<ChunkSupplier.Chunk> {
        private final DataSource[] dataSources;
        private final int[] chunkCounts;
        private final int totalChunkCount;
        private final AtomicInteger nextIndex;

        private ChunkSupplier(DataSource[] dataSources) {
            this.dataSources = dataSources;
            chunkCounts = new int[dataSources.length];
            int totalChunkCount = 0;
            for (int i = 0; i < dataSources.length; i++) {
                long chunkCount = getChunkCount(dataSources[i].size(),
                        CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
                if (chunkCount > Integer.MAX_VALUE) {
                    throw new RuntimeException(
                            String.format(
                                    "Number of chunks in dataSource[%d] is greater than max int.",
                                    i));
                }
                //记录每部分的块数
                chunkCounts[i] = (int)chunkCount;
                //记录总的块数
                totalChunkCount = (int) (totalChunkCount + chunkCount);
            }
            this.totalChunkCount = totalChunkCount;
            // 用于多线程
            nextIndex = new AtomicInteger(0);
        }

        /**
         * We map an integer index to the termination-adjusted dataSources 1MB chunks.
         * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned
         * blocks in each input {@link DataSource} (unless the DataSource itself is
         * 1MB-aligned).
         */
        @Override
        public ChunkSupplier.Chunk get() {
            int index = nextIndex.getAndIncrement();
            if (index < 0 || index >= totalChunkCount) {
                return null;
            }

            int dataSourceIndex = 0;
            long dataSourceChunkOffset = index;
            for (; dataSourceIndex < dataSources.length; dataSourceIndex++) {
                //要是dataSourceChunkOffset没超过dataSourceIndex指定的这一块的块数,那么, 说明就是dataSourceIndex指定的区块
                if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) {
                    break;
                }
                // 要是dataSourceChunkOffset超过了dataSourceIndex当前指定的这个数据源的块数,
                // 那么移到下一个数据源,同时dataSourceChunkOffset的计数要减去当前这个数据源的块数
                dataSourceChunkOffset -= chunkCounts[dataSourceIndex];
            }

            //剩余的大小取:1M 与 dataSourceIndex这一个数据源剩下的大小两者的最小值
            long remainingSize = Math.min(
                    dataSources[dataSourceIndex].size() -
                            dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES,
                    CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);

            final int size = (int)remainingSize;
            final ByteBuffer buffer = ByteBuffer.allocate(size);
            try {
                dataSources[dataSourceIndex].copyTo(
                        dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size,
                        buffer);
            } catch (IOException e) {
                throw new IllegalStateException("Failed to read chunk", e);
            }
            buffer.rewind();

            return new Chunk(index, buffer, size);
        }

        static class Chunk {
            private final int chunkIndex;
            private final ByteBuffer data;
            private final int size;

            private Chunk(int chunkIndex, ByteBuffer data, int size) {
                this.chunkIndex = chunkIndex;
                this.data = data;
                this.size = size;
            }
        }
    }

ChunkDigester这个类主要用来计算chunk摘要,然后将计算出来的值拼接起来保存到 chunkDigest.concatOfDigestsOfChunks

/**
     * A per-thread digest worker.
     */
    private static class ChunkDigester implements Runnable {
        private final ChunkSupplier dataSupplier;
        private final List<ChunkDigests> chunkDigests;
        private final List<MessageDigest> messageDigests;
        private final DataSink mdSink;

        private ChunkDigester(ChunkSupplier dataSupplier, List<ChunkDigests> chunkDigests) {
            this.dataSupplier = dataSupplier;
            this.chunkDigests = chunkDigests;
            messageDigests = new ArrayList<>(chunkDigests.size());
            for (ChunkDigests chunkDigest : chunkDigests) {
                try {
                    messageDigests.add(chunkDigest.createMessageDigest());
                } catch (NoSuchAlgorithmException ex) {
                    throw new RuntimeException(ex);
                }
            }
            mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0]));
        }

        @Override
        public void run() {
            byte[] chunkContentPrefix = new byte[5];
            chunkContentPrefix[0] = (byte) 0xa5;

            try {
                for (ChunkSupplier.Chunk chunk = dataSupplier.get();
                     chunk != null;
                     chunk = dataSupplier.get()) {
                    int size = chunk.size;
                    if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) {
                        throw new RuntimeException("Chunk size greater than expected: " + size);
                    }

                    // 设置块长度到chunkContentPrefix字节数组后4字节中
                    setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1);
                    // 将chunkContentPrefix字节数组加入计算摘要
                    mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length);

                    // 将块数据加入计算摘要
                    mdSink.consume(chunk.data);

                    // Now finalize chunk for all algorithms.
                    for (int i = 0; i < chunkDigests.size(); i++) {
                        ChunkDigests chunkDigest = chunkDigests.get(i);
                        //计算{0xa5, 4-bytes-of-little-endian-chunk-size, chunk-data}的摘要值,保存到chunkDigest.concatOfDigestsOfChunks数组中
                        // 从chunkDigest.getOffset(chunk.chunkIndex)开始,一共chunkDigest.digestOutputSize字节空间中
                        int actualDigestSize = messageDigests.get(i).digest(
                                chunkDigest.concatOfDigestsOfChunks,
                                chunkDigest.getOffset(chunk.chunkIndex),
                                chunkDigest.digestOutputSize);
                        if (actualDigestSize != chunkDigest.digestOutputSize) {
                            throw new RuntimeException(
                                    "Unexpected output size of " + chunkDigest.algorithm
                                            + " digest: " + actualDigestSize);
                        }
                    }
                }
            } catch (IOException | DigestException e) {
                throw new RuntimeException(e);
            }
        }
    }

并行计算类RunnaleExecutor用于控制多线程并行计算每一块的摘要值,并等待所有线程计算结束才继续后面的代码逻辑

 static final RunnablesExecutor MULTI_THREADED = new RunnablesExecutor() {
        private final int PARALLELISM = Math.min(32, Runtime.getRuntime().availableProcessors());
        private final int QUEUE_SIZE = 4;

        @Override
        public void execute(RunnablesProvider provider) {
            final ExecutorService mExecutor =
                    new ThreadPoolExecutor(PARALLELISM, PARALLELISM,
                            0L, MILLISECONDS,
                            new ArrayBlockingQueue<>(QUEUE_SIZE),
                            new ThreadPoolExecutor.CallerRunsPolicy());

            //多线程同步控制器
            Phaser tasks = new Phaser(1);

            //创建PARALLELISM个任务并行执行
            for (int i = 0; i < PARALLELISM; ++i) {
                Runnable task = new Runnable() {
                    @Override
                    public void run() {
                        //v2签名对应的Runable实现类为ApkSigningBlockUtils#ChunkDigester
                        Runnable r = provider.createRunnable();
                        r.run();
                        //当前线程立即返回下一阶段的序号,并且从Phaser中移出当前线程,其他线程在调用arriveAndAwaitAdvance()时不需要等待当前线程.
                        tasks.arriveAndDeregister();
                    }
                };
                // 线程计数parties +1
                tasks.register();

                mExecutor.execute(task);
            }

            // Waiting for the tasks to complete.
            // 表示当前线程完成当前阶段的任务,等待其他线程完成当前阶段的任务.
            // 等待线程的数量就是tasks.register()注册的数量(即parties的数值),只有调用了parties次 arriveAndDeregister(),才会继续执行后面的代码
            tasks.arriveAndAwaitAdvance();

            mExecutor.shutdownNow();
        }
    };
  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值