【前言】
?????V1签名作为一种历史悠久的签名方式,弊端也是比较明显的,一方面由于V1签名 是对Apk内的单个文件 逐一计算摘要进行签名校验的,所以要是Apk内的文件比较多,计算速度是非常慢 的,同时又因为只对单个文件的完整性进行校验,那么对apk压缩包包体进行篡改的话,签名依然还是可以校验通过,完整性的校验工作做得不够到位。到了Android 7.0 ,V2签名方式就应运而生,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;
public static byte[] generateApkSigningBlock(
List<Pair<byte[], Integer>> apkSignatureSchemeBlockPairs) {
int blocksSize = 0;
for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) {
blocksSize += 8 + 4 + schemeBlockPair.getFirst().length;
}
int resultSize =
8
+ blocksSize
+ 8
+ 16
;
ByteBuffer paddingPair = null;
if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
(resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
if (padding < 12) {
padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
}
paddingPair = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN);
paddingPair.putLong(padding - 8);
paddingPair.putInt(VERITY_PADDING_BLOCK_ID);
paddingPair.rewind();
resultSize += padding;
}
ByteBuffer result = ByteBuffer.allocate(resultSize);
result.order(ByteOrder.LITTLE_ENDIAN);
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;
result.putLong(pairSizeFieldValue);
result.putInt(apkSignatureSchemeId);
result.put(apkSignatureSchemeBlock);
}
if (paddingPair != null) {
result.put(paddingPair);
}
result.putLong(blockSizeFieldValue);
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签名分块 :
static Pair<ByteBuffer, Long> findApkSigningBlock(
RandomAccessFile apk, long centralDirOffset)
throws IOException, SignatureNotFoundException {
if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
throw new SignatureNotFoundException(
"APK too small for APK Signing Block. ZIP Central Directory offset: "
+ centralDirOffset);
}
ByteBuffer footer = ByteBuffer.allocate(24);
footer.order(ByteOrder.LITTLE_ENDIAN);
apk.seek(centralDirOffset - footer.capacity());
apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity());
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
throw new SignatureNotFoundException(
"No APK Signing Block before ZIP Central Directory");
}
long apkSigBlockSizeInFooter = footer.getLong(0);
if ((apkSigBlockSizeInFooter < footer.capacity())
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
throw new SignatureNotFoundException(
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
}
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
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.seek(apkSigBlockOffset);
apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity());
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签名分块整体的大小totalSize ,APK签名分块开始位置apkSigBlockOffset = ZIP中央目录开始位置centralDirOffset - APK签名分块整体的大小totalSize Ⅳ、从APK签名分块开始位置apkSigBlockOffset 开始,读取8个字节 数据,这个值就是头部记录的APK签名分块大小apkSigBlockSizeInHeader Ⅴ、假如 头部记录的APK签名分块大小apkSigBlockSizeInHeader = 尾部记录的APK签名分块大小apkSigBlockSizeInFooter ,那么从APK签名分块开始位置apkSigBlockOffset 开始,读取APK签名分块整体的大小totalSize 个字节数据,这就是整个APK签名分块数据 接下来看看怎么从APK签名分块中找到V2签名信息 :
static ByteBuffer findApkSignatureSchemeBlock(ByteBuffer apkSigningBlock, int blockId)
throws SignatureNotFoundException {
checkByteOrderLittleEndian(apkSigningBlock);
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
int entryCount = 0;
while (pairs.hasRemaining()) {
entryCount++;
if (pairs.remaining() < 8) {
throw new SignatureNotFoundException(
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
}
long lenLong = pairs.getLong();
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());
}
int id = pairs.getInt();
if (id == blockId) {
return getByteBuffer(pairs, len - 4);
}
pairs.position(nextEntryPos);
}
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摘要计算代码片段(关键部分加了注释)
static void computeOneMbChunkContentDigests(
RunnablesExecutor executor,
Set<ContentDigestAlgorithm> digestAlgorithms,
DataSource[] contents,
Map<ContentDigestAlgorithm, byte[]> outputContentDigests)
throws NoSuchAlgorithmException, DigestException {
long chunkCountLong = 0;
for (DataSource input : contents) {
chunkCountLong += getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES);
}
if (chunkCountLong > Integer.MAX_VALUE) {
throw new DigestException("Input too long: " + chunkCountLong + " chunks");
}
int chunkCount = (int) chunkCountLong;
List<ChunkDigests> chunkDigestsList = new ArrayList<>(digestAlgorithms.size());
for (ContentDigestAlgorithm algorithms : digestAlgorithms) {
chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount));
}
ChunkSupplier chunkSupplier = new ChunkSupplier(contents);
executor.execute(new RunnablesProvider() {
@Override
public Runnable createRunnable() {
return new ChunkDigester(chunkSupplier, chunkDigestsList);
}
});
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];
concatOfDigestsOfChunks[0] = 0x5a;
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),计算可以分割的块数, 实现逻辑如下:
private static long getChunkCount(long inputSize, long chunkSize) {
return (inputSize + chunkSize - 1) / chunkSize;
}
ChunkSupplier 这个类是用来构造返回每一块的数据,可以多线程进行操作
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);
}
@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++) {
if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) {
break;
}
dataSourceChunkOffset -= chunkCounts[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 中
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);
}
setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1);
mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length);
mdSink.consume(chunk.data);
for (int i = 0; i < chunkDigests.size(); i++) {
ChunkDigests chunkDigest = chunkDigests.get(i);
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);
for (int i = 0; i < PARALLELISM; ++i) {
Runnable task = new Runnable() {
@Override
public void run() {
Runnable r = provider.createRunnable();
r.run();
tasks.arriveAndDeregister();
}
};
tasks.register();
mExecutor.execute(task);
}
tasks.arriveAndAwaitAdvance();
mExecutor.shutdownNow();
}
};
|