美团多渠道打包Walle源码解析

apk学习系列

Posted by Cc1over on July 31, 2019

美团多渠道打包Walle源码解析

前言

最近暑假一直在等美工出图,没有角度做项目,看了不少android源码情景分析但是又没有写笔记的头绪,最近从Walle开始记录下apk知识学习笔记

[GradlePlugin-> apply]

void apply(Project project) {
     
        applyExtension(project);
        applyTask(project);
    }

[GradlePlugin-> applyExtension]

public static final String sPluginExtensionName = "walle";

void applyExtension(Project project) {
        project.extensions.create(sPluginExtensionName, Extension, project);
    }

[GradlePlugin->applyTask]

void applyTask(Project project) {
        project.afterEvaluate {
            project.android.applicationVariants.all { BaseVariant variant ->
                def variantName = variant.name.capitalize();

                if (!isV2SignatureSchemeEnabled(variant)) { // 1
                    throw new ProjectConfigurationException("Plugin requires 'APK Signature Scheme v2 Enabled' for ${variant.name}.", null);
                }

                ChannelMaker channelMaker = project.tasks.create("assemble${variantName}Channels", ChannelMaker);
                channelMaker.targetProject = project;
                channelMaker.variant = variant;
                channelMaker.setup();

                if (variant.hasProperty('assembleProvider')) { // 2
                    channelMaker.dependsOn variant.assembleProvider.get()
                } else {
                    channelMaker.dependsOn variant.assemble
                }
            }
        }
    }
  • isV2SignatureSchemeEnabled: 采用gradle的方式校验当前project是否支持v2签名
  • 注释2:channelMaker任务依赖于assemble任务

[ChannelMaker-> packaging]

 @TaskAction
    public void packaging() {
        
        while (iterator.hasNext()) {
           
            checkV2Signature(apkFile) // 1
                
            def nameVariantMap = [
                    'appName'    : targetProject.name,
                    'projectName': targetProject.rootProject.name,
                    'buildType'  : variant.buildType.name,
                    'versionName': variant.versionName,
                    'versionCode': variant.versionCode,
                    'packageName': variant.applicationId,
                    'flavorName' : variant.flavorName
            ]

            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)
            }
        }

        targetProject.logger.lifecycle("APK Signature Scheme v2 Channel Maker takes about " + (
                System.currentTimeMillis() - startTime) + " milliseconds");
    }
  • checkV2Signature: 检查apk采用的是v1签名还是v2签名
  • 以下的逻辑就是采取一些判断然后最终调到generateChannelApkByChannelFile方法执行生成apk,这些判断与外界的调用方式相关,包括局部渠道打包,写入额外信息,用channelFile生成渠道包等

[ChannelMaker-> checkV2Signature]

 def checkV2Signature(File apkFile) {
        FileInputStream fIn;
        FileChannel fChan;
        try {
            fIn = new FileInputStream(apkFile);
            fChan = fIn.getChannel();
            long fSize = fChan.size();
            ByteBuffer byteBuffer = ByteBuffer.allocate((int) fSize);
            fChan.read(byteBuffer);
            byteBuffer.rewind();

            DataSource dataSource = new ByteBufferDataSource(byteBuffer);

            ApkVerifier apkVerifier = new ApkVerifier();
            ApkVerifier.Result result = apkVerifier.verify(dataSource, 0);
            if (!result.verified || !result.verifiedUsingV2Scheme) {
                throw new GradleException("${apkFile} has no v2 signature in Apk Signing Block!");
            }
        } catch (IOException ignore) {
            ignore.printStackTrace();
        } finally {
            IOUtils.closeQuietly(fChan);
            IOUtils.closeQuietly(fIn);
        }
    }

[ApkVerifier-> verify]

 public Result verify(DataSource apk, int minSdkVersion) throws IOException, ZipFormatException {
       
        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);  // 1
     
        Result result = new Result();
        try {
            V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
            result.mergeFrom(v2Result);
        } catch (V2SchemeVerifier.SignatureNotFoundException ignored) {}
        if (result.containsErrors()) {
            return result;
        }

        return result;
    }
  • 注释1: 看到这里暂时还是很懵,不确定这个zipSections是什么,所以先看这个findZipSections做什么事情

[ApkUtils-> findZipSections]

public static ZipSections findZipSections(DataSource apk)
            throws IOException, ZipFormatException {
        Pair<ByteBuffer, Long> eocdAndOffsetInFile =
                ZipUtils.findZipEndOfCentralDirectoryRecord(apk); // 1
        if (eocdAndOffsetInFile == null) {
            throw new ZipFormatException("ZIP End of Central Directory record not found");
        }

        ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
        long eocdOffset = eocdAndOffsetInFile.getSecond();
        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
            throw new ZipFormatException("ZIP64 APK not supported");
        }
        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
        long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
        if (cdStartOffset >= eocdOffset) {
            throw new ZipFormatException(
                    "ZIP Central Directory start offset out of range: " + cdStartOffset
                        + ". ZIP End of Central Directory offset: " + eocdOffset);
        }

        long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
        long cdEndOffset = cdStartOffset + cdSizeBytes;
        if (cdEndOffset > eocdOffset) {
            throw new ZipFormatException(
                    "ZIP Central Directory overlaps with End of Central Directory"
                            + ". CD end: " + cdEndOffset
                            + ", EoCD start: " + eocdOffset);
        }

        int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);

        return new ZipSections(
                cdStartOffset,
                cdSizeBytes,
                cdRecordCount,
                eocdOffset,
                eocdBuf);
  • 注释1: 这里的第一步从方法名可以看出来是先要找到zip文件的EOCD区域

[ZipUtils-> findZipEndOfCentralDirectoryRecord]

private static final int ZIP_EOCD_REC_MIN_SIZE = 22; 
public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
            throws IOException {  
        // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
        // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
        // reading more data.
        Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0); // 1
        if (result != null) {
            return result;
        }
      return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); // 2
}
  • 注释1: 看到google工程师的注释,看到这里传了一个注释的长度去调用findZipEndOfCentralDirectoryRecord这个方法,而传0是一种优化的策略,依然有点小懵逼

[ZipUtils-> findZipEndOfCentralDirectoryRecord]

 private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
            DataSource zip, int maxCommentSize) throws IOException {
     
        // Lower maxCommentSize if the file is too small.
        maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); // 1

        int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize; 
        long bufOffsetInFile = fileSize - maxEocdSize;
        ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize); // 2
        buf.order(ByteOrder.LITTLE_ENDIAN);
        int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); // 3
        if (eocdOffsetInBuf == -1) { 
            // No EoCD record found in the buffer
            return null;
        }
        // EoCD found
        buf.position(eocdOffsetInBuf);
        ByteBuffer eocd = buf.slice();
        eocd.order(ByteOrder.LITTLE_ENDIAN);
        return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf);
    }
  • 注释1: 从这段逻辑可以看得到,第一次调用这个方法的时候这个值会为0
  • 注释2: 看到这行代码之后其实就把前面的懵逼给解开了,因为看到这里才忽然意识到,这个apk签名信息的读取操作其实是从后往前计算EOCD的长度的,所以上一个方法findZipEndOfCentralDirectoryRecord才会需要去筛选comment的长度,而大部分apk这个值是0,所以google工程师才会把它称为一种优化
  • 注释3: 因为上一个方法findZipEndOfCentralDirectoryRecord它其实也只是试探性的把0,也就是没有comment这种情况做读取,但是这也有可能是错误的,所以findZipEndOfCentralDirectoryRecord肯定就是一种校验机制去判断上一步的猜测有没有误

[ZipUtils-> findZipEndOfCentralDirectoryRecord]

private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
   
        int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
        int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
        for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
                expectedCommentLength++) {
            int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
            if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
                int actualCommentLength =
                        getUnsignedInt16(
                                zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
                if (actualCommentLength == expectedCommentLength) {
                    return eocdStartPos;
                }
            }
        }

        return -1;
   
  • 这个方法的逻辑就是从0开始试验comment的长度,然后计算EOCD的起始位置,再然后读取最开始的4个字节,如果与EOCD的magic字段相等就说明我们上一步传进来的buf是正确的EOCD

[ZipUtils-> findZipEndOfCentralDirectoryRecord]

private static final int ZIP_EOCD_REC_MIN_SIZE = 22; 
public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
            throws IOException {  
        // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
        // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
        // reading more data.
        Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0); // 1
        if (result != null) {
            return result;
        }
      return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); // 2
}
  • 回到最开始的入口方法findZipEndOfCentralDirectoryRecord,回看注释2,其实看完了上面的几个流程,回过头来看这个方法,就发现逻辑很简单,只不过就是先用0的comment长度去尝试,再去算实际的commend长度尝试

[ApkUtils-> findZipSections]

public static ZipSections findZipSections(DataSource apk)
            throws IOException, ZipFormatException {
        Pair<ByteBuffer, Long> eocdAndOffsetInFile =
                ZipUtils.findZipEndOfCentralDirectoryRecord(apk); // 1
        if (eocdAndOffsetInFile == null) {
            throw new ZipFormatException("ZIP End of Central Directory record not found");
        }

        ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
        long eocdOffset = eocdAndOffsetInFile.getSecond();
        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
            throw new ZipFormatException("ZIP64 APK not supported");
        }
        eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
        long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); // 2
        if (cdStartOffset >= eocdOffset) {
            throw new ZipFormatException(
                    "ZIP Central Directory start offset out of range: " + cdStartOffset
                        + ". ZIP End of Central Directory offset: " + eocdOffset);
        }

        long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); // 3
        long cdEndOffset = cdStartOffset + cdSizeBytes; // 4
        if (cdEndOffset > eocdOffset) {
            throw new ZipFormatException(
                    "ZIP Central Directory overlaps with End of Central Directory"
                            + ". CD end: " + cdEndOffset
                            + ", EoCD start: " + eocdOffset);
        }

        int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); // 5

        return new ZipSections(
                cdStartOffset,
                cdSizeBytes,
                cdRecordCount,
                eocdOffset,
                eocdBuf);
  • 回到开始的目标findZipSections方法,从注释1已经找到了EOCD区域在整个apk文件位置,然后就通过设计好的Pair数据结构拿到对象的eocd和入口位置
  • 注释2: 这里就是根据EOCD来获取Central Directory的偏移位置
  • 注释3: 这里就是根据EOCD来获取Central Directory的长度
  • 注释4: 这里就根据Central Directory的偏移位置和长度来计算出Central Directory结束位置
  • 注释5: 这里是根据EOCD来获取Central Directory的中央目录结构总数

阶段小结

上面的整个流程其实也只是从后往前读,然后通过EOCD去获取Central Directory的各种信息,然后构建一个ZipSections对象返回出去,所以其实之前的问题也得到了解答,那就是ZipSections这个类其实就是一个Zip文件信息包装类

public static class ZipSections {
        private final long mCentralDirectoryOffset;
        private final long mCentralDirectorySizeBytes;
        private final int mCentralDirectoryRecordCount;
        private final long mEocdOffset;
        private final ByteBuffer mEocd;

        public ZipSections(
                long centralDirectoryOffset,
                long centralDirectorySizeBytes,
                int centralDirectoryRecordCount,
                long eocdOffset,
                ByteBuffer eocd) {
            mCentralDirectoryOffset = centralDirectoryOffset;
            mCentralDirectorySizeBytes = centralDirectorySizeBytes;
            mCentralDirectoryRecordCount = centralDirectoryRecordCount;
            mEocdOffset = eocdOffset;
            mEocd = eocd;
        }
  • 中央目录偏移位置
  • 中央目录长度
  • 中央目录结构总数
  • EOCD偏移位置
  • EOCD数据区

[ApkVerifier-> verify]

 public Result verify(DataSource apk, int minSdkVersion) throws IOException, ZipFormatException {
       
        ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);  // 1
     
        Result result = new Result();
        try {
            V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
            result.mergeFrom(v2Result);
        } catch (V2SchemeVerifier.SignatureNotFoundException ignored) {}
        if (result.containsErrors()) {
            return result;
        }

        return result;
    }
  • 从注释1获取到这些信息之后就传给V2SchemeVerifier对象进行校验

[V2SchemeVerifier-> verify]

public static Result verify(DataSource apk, ApkUtils.ZipSections zipSections)
            throws IOException, SignatureNotFoundException {
        Result result = new Result();
        SignatureInfo signatureInfo = findSignature(apk, zipSections, result);  // 1

        DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
        DataSource centralDir =
                apk.slice(
                        signatureInfo.centralDirOffset,
                        signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
        ByteBuffer eocd = signatureInfo.eocd;

        verify(beforeApkSigningBlock,
                signatureInfo.signatureBlock,
                centralDir,
                eocd,
                result);
        return result;
    }

[V2SchemeVerifier-> findSignature]

 private static SignatureInfo findSignature(
            DataSource apk, ApkUtils.ZipSections zipSections, Result result)
                    throws IOException, SignatureNotFoundException {
   
        ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory();
        Pair<ByteBuffer, Long> apkSigningBlockAndOffset =
                findApkSigningBlock(apk, centralDirStartOffset); // 1
        ByteBuffer apkSigningBlock = apkSigningBlockAndOffset.getFirst();
        long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();

        // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
        ByteBuffer apkSignatureSchemeV2Block =
                findApkSignatureSchemeV2Block(apkSigningBlock, result);

        return new SignatureInfo(
                apkSignatureSchemeV2Block,
                apkSigningBlockOffset,
                centralDirStartOffset,
                eocdStartOffset,
                eocd);
    }

[V2SchemeVerifier-> findApkSigningBlock]

 public static Pair<ByteBuffer, Long> findApkSigningBlock(
            DataSource 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
     
        // Read the magic and offset in file from the footer section of the block:
        // * uint64:   size of block
        // * 16 bytes: magic
        ByteBuffer footer = apk.getByteBuffer(centralDirOffset - 24, 24); // 1
        footer.order(ByteOrder.LITTLE_ENDIAN);
        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");
        }
        // Read and compare size fields
        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 = apk.getByteBuffer(apkSigBlockOffset, totalSize);
        apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
        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.of(apkSigBlock, apkSigBlockOffset);
    }
  • 注释1: 这里也是一个从后往前读的过程,从Central Directory的偏移量开始往前偏移24个字节,读出的值就是APK Signing Block的长度以及对应的区域magic
  • 剩下的逻辑就是很简单了,根据Central Directory的偏移量以及读出来Signing Block的长度,计算出Signing Block的起始偏移量,然后读进ByteBuffer返回

[V2SchemeVerifier-> findSignature]

 private static SignatureInfo findSignature(
            DataSource apk, ApkUtils.ZipSections zipSections, Result result)
                    throws IOException, SignatureNotFoundException {
   
        ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory();
        Pair<ByteBuffer, Long> apkSigningBlockAndOffset =
                findApkSigningBlock(apk, centralDirStartOffset); // 1
        ByteBuffer apkSigningBlock = apkSigningBlockAndOffset.getFirst();
        long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();

        // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
        ByteBuffer apkSignatureSchemeV2Block =
                findApkSignatureSchemeV2Block(apkSigningBlock, result); // 2

        return new SignatureInfo(
                apkSignatureSchemeV2Block,
                apkSigningBlockOffset,
                centralDirStartOffset,
                eocdStartOffset,
                eocd);
    }

[V2SchemeVerifier-> findApkSignatureSchemeV2Block]

 public static ByteBuffer findApkSignatureSchemeV2Block(
            ByteBuffer apkSigningBlock,
            Result result) 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
        ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); // 1

        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 == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {  // 2
                return getByteBuffer(pairs, len - 4);
            }
            result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
            pairs.position(nextEntryPos);
        }

        throw new SignatureNotFoundException(
                "No APK Signature Scheme v2 block in APK Signing Block");
    }
  • 注释1: 前读8个字节后读24个字节,读出Signing Block中的pair数据区
  • 注释2: 拿到了pairs中的一个id,这个id就是整个校验V2签名的核心,如果这个地方**id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID ** 就会返回pairs中对应这个id的值数据,对应v2 SigningBlock,继续进行校验

[V2SchemeVerifier-> verify]

public static Result verify(DataSource apk, ApkUtils.ZipSections zipSections)
            throws IOException, SignatureNotFoundException {
        Result result = new Result();
        SignatureInfo signatureInfo = findSignature(apk, zipSections, result);  // 1

        DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
        DataSource centralDir =
                apk.slice(
                        signatureInfo.centralDirOffset,
                        signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
        ByteBuffer eocd = signatureInfo.eocd;

        verify(beforeApkSigningBlock,                  // 2
                signatureInfo.signatureBlock,
                centralDir,
                eocd,
                result);
        return result;
    }
  • 注释2: 起来来到注释2这个地方的时候已经知道apk是否采用了v2签名,这里就是剩余的校验工作,去验证提供的V2签名以及是根据签名信息去校验

[ChannelMaker-> packaging]

@TaskAction
    public void packaging() {
        
        while (iterator.hasNext()) {
           
            checkV2Signature(apkFile) // 1
                
            def nameVariantMap = [
                    'appName'    : targetProject.name,
                    'projectName': targetProject.rootProject.name,
                    'buildType'  : variant.buildType.name,
                    'versionName': variant.versionName,
                    'versionCode': variant.versionCode,
                    'packageName': variant.applicationId,
                    'flavorName' : variant.flavorName
            ]

            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)
            }
        }

        targetProject.logger.lifecycle("APK Signature Scheme v2 Channel Maker takes about " + (
                System.currentTimeMillis() - startTime) + " milliseconds");
    }

[ChannelMaker-> generateChannelApkByChannelFile ]

def generateChannelApkByChannelFile(File channelFile, File apkFile, File channelOutputFolder, nameVariantMap) {
        getChannelListFromFile(channelFile).each { channel -> generateChannelApk(apkFile, channelOutputFolder, nameVariantMap, channel, null, null) }
    }

[ChannelMaker-> generateChannelApk]

def generateChannelApk(File apkFile, File channelOutputFolder, Map nameVariantMap, channel, extraInfo, alias) {
        Extension extension = Extension.getConfig(targetProject);

        def buildTime = new SimpleDateFormat('yyyyMMdd-HHmmss').format(new Date());
        def channelName = alias == null ? channel : alias

        String fileName = apkFile.getName();
        if (fileName.endsWith(DOT_APK)) {
            fileName = fileName.substring(0, fileName.lastIndexOf(DOT_APK)); 
        }

        String apkFileName = "${fileName}-${channelName}${DOT_APK}";

        File channelApkFile = new File(apkFileName, channelOutputFolder);
        FileUtils.copyFile(apkFile, channelApkFile);
        ChannelWriter.put(channelApkFile, channel, extraInfo)

        nameVariantMap.put("buildTime", buildTime);
        nameVariantMap.put('channel', channelName);
        nameVariantMap.put('fileSHA1', getFileHash(channelApkFile));
        if (extension.apkFileNameFormat != null && extension.apkFileNameFormat.length() > 0) {
            def newApkFileName = new SimpleTemplateEngine().createTemplate(extension.apkFileNameFormat).make(nameVariantMap).toString()
            if (!newApkFileName.contentEquals(apkFileName)) {
                channelApkFile.renameTo(new File(newApkFileName, channelOutputFolder))
            }
        }
    }

[ChannelWriter-> put ]

public static void put(final File apkFile, final String channel, final Map<String, String> extraInfo, final boolean lowMemory) throws IOException, SignatureNotFoundException {
        final Map<String, String> newData = new HashMap<String, String>();
        final Map<String, String> existsData = ChannelReader.getMap(apkFile); // 1
        if (existsData != null) {
            newData.putAll(existsData);
        }
        if (extraInfo != null) {
            // can't use
            extraInfo.remove(ChannelReader.CHANNEL_KEY);
            newData.putAll(extraInfo);
        }
        if (channel != null && channel.length() > 0) {
            newData.put(ChannelReader.CHANNEL_KEY, channel);
        }
        final JSONObject jsonObject = new JSONObject(newData);
        putRaw(apkFile, jsonObject.toString(), lowMemory);
    }
  • 注释1: 这里就是通过ChannelReader去获取到apk文件中的已有的渠道信息,这里其实设计还是挺吸引我的,因为其实这里Walle的结构是这样的:
    • ChannelReader 负责做数据的处理,把原来的Json格式字符串转换成Map
    • PayloadReader 负责真正从apk中获取渠道信息
    • 这种设计其实感觉和IO的设计有异曲同工之妙
    • 所以其实ChannelWriter也是同样镜像的设计

[PayloadReader-> getAll]

private static Map<Integer, ByteBuffer> getAll(final File apkFile) {
        Map<Integer, ByteBuffer> idValues = null;
        RandomAccessFile randomAccessFile = null;
        FileChannel fileChannel = null;
        randomAccessFile = new RandomAccessFile(apkFile, "r");
        fileChannel = randomAccessFile.getChannel();
        // 1
        final ByteBuffer apkSigningBlock2 = ApkUtil.findApkSigningBlock(fileChannel).getFirst(); 
        // 2 
        idValues = ApkUtil.findIdValues(apkSigningBlock2);
        return idValues;
    }

[ApkUtil-> findApkSigningBlock ]

public static Pair<ByteBuffer, Long> findApkSigningBlock(
            final FileChannel fileChannel) throws IOException, SignatureNotFoundException {
        final long centralDirOffset = findCentralDirStartOffset(fileChannel);
        return findApkSigningBlock(fileChannel, centralDirOffset);
    }
  • 这个ApkUtil和上面的ApkUtils不一样,上面的ApkUtils是Android的开源代码,而ApkUtil则是美团自己开发的Apk工具类
  • 这里有一个设计的点,那就是其实这个ApkUtil里的方法或者找EOCD或者Central Diretory的方法和google开源的ApkUtils是相似的,而不直接用ApkUtils是因为这个工具类里面提供的操作不足够,而笔者认为最舒服的做法就是按照google提供的源码,摘抄或者借鉴我们需要的部分,然后以此加入部分操作,以这种方式来满足我们的需求,其实像微信AndResGuard的设计也是如此,值得学习
  • 这个方法的返回就是一个Signing Block以及对应的偏移

[ApkUtil-> findIdValues]

public static Map<Integer, ByteBuffer> findIdValues(final ByteBuffer apkSigningBlock) throws SignatureNotFoundException {
    
            final int id = pairs.getInt();
            idValues.put(id, getByteBuffer(pairs, len - 4));
            pairs.position(nextEntryPos);
        }
        return idValues;
    }

[PayloadReader-> get]

public static byte[] get(final File apkFile, final int id) {
        final Map<Integer, ByteBuffer> idValues = getAll(apkFile);
        if (idValues == null) {
            return null;
        }
        final ByteBuffer byteBuffer = idValues.get(id);
        if (byteBuffer == null) {
            return null;
        }
        return getBytes(byteBuffer);
    }
  • 这个方法的逻辑就是根据传进来的id从Sign Block中取到对应的取到信息,所以其实看到这里我们也就知道其实Walle的渠道信息是放置在Signing Block中的
  • 从ByteBuffer中取出相应的字节然后返回

[PayloadReader-> getString]

public static String getString(final File apkFile, final int id) {
        final byte[] bytes = PayloadReader.get(apkFile, id);
        if (bytes == null) {
            return null;
        }
        try {
            return new String(bytes, ApkUtil.DEFAULT_CHARSET);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }
  • 这里就是ChannelReader真正调用的地方,拿到一个id所对应数据的value的String
  • 为什么这里要这样做呢?其实笔者也踩过这个坑,那就是如果不限制这个字符串的编码格式,出来的字符串很可能是乱码
  • 梳理一下整个ChannelReader调用PayloadReader获取相应id取到信息的过程:
    • ChannelReader.getMap(apkFile) ChannelReader把从PayloadReader中获取的数据包装成Map
    • PayloadReader.getString(apkFile,id) PayloadReader把读取的字节数据包装成UTF-8的字符串
    • PayloadReader.get(apkFile,id) PayloadReader获取apk中Signing Block然后根据传入的id找到相应的数据

阶段小结

其实笔者看到这里的时候产生的错觉,那就是来到这一步我已经知道Walle是怎么写入数据的了,把渠道信息存在哪里,怎么读取,写入应该是镜像的,还需要看下去?其实现在想想真的是个错觉,因为写入会有一个细节被忽略了,那就是Signing Block的长度改变了之后,那EOCD里面中央目录的偏移量不是会变吗?而V2签名保护的是1、3、4部分那这为什么能成功,其次就是写入的时候一个参数lowMemory还没用到过,它到底是怎么用的,带着这个问题继续去看ChannelWriter的写入操作

[ChannelWriter-> putRaw]

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-> put]

public static void put(final File apkFile, final int id, final String string, final boolean lowMemory) throws IOException, SignatureNotFoundException {
        final byte[] bytes = string.getBytes(ApkUtil.DEFAULT_CHARSET);
        final ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
        byteBuffer.put(bytes, 0, bytes.length);
        byteBuffer.flip();
        put(apkFile, id, byteBuffer, lowMemory);
    }

[PayloadWriter-> put]

public static void put(final File apkFile, final int id, final ByteBuffer buffer, final boolean lowMemory) throws IOException, SignatureNotFoundException {
        final Map<Integer, ByteBuffer> idValues = new HashMap<Integer, ByteBuffer>();
        idValues.put(id, buffer);
        putAll(apkFile, idValues, lowMemory);
    }

[PayloadWriter-> putAll]

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);
    }
  • 这个handle方法初步看应该是生成一个新的Signing Block,作为一种回调给调用它的方法,这种设计还是挺优秀的

[PayloadWriter-> handleApkSigningBlock]

 static void handleApkSigningBlock(final File apkFile, final ApkSigningBlockHandler handler, final boolean lowMemory) throws IOException, SignatureNotFoundException {
       
            final Map<Integer, ByteBuffer> originIdValues = ApkUtil.findIdValues(apkSigningBlock2);
            // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
            final ByteBuffer apkSignatureSchemeV2Block = originIdValues.get(ApkUtil.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);

            if (apkSigningBlockOffset != 0 && centralDirStartOffset != 0) {

                // read CentralDir
                fIn.seek(centralDirStartOffset);

                byte[] centralDirBytes = null;
                File tempCentralBytesFile = null;
                // read CentralDir
                if (lowMemory) {
                    tempCentralBytesFile = new File(apkFile.getParent(), UUID.randomUUID().toString());
                    FileOutputStream outStream = null;
                    try {
                        outStream = new FileOutputStream(tempCentralBytesFile);
                        final byte[] buffer = new byte[1024];

                        int len;
                        while ((len = fIn.read(buffer)) > 0){
                            outStream.write(buffer, 0, len);
                        }
                    } finally {
                        if (outStream != null) {
                            outStream.close();
                        }
                    }
                } else {
                    centralDirBytes = new byte[(int) (fileChannel.size() - centralDirStartOffset)];
                    fIn.read(centralDirBytes);
                }

                //update apk sign
                fileChannel.position(apkSigningBlockOffset);
                final long length = apkSigningBlock.writeApkSigningBlock(fIn);

                // update CentralDir
                if (lowMemory) {
                    FileInputStream inputStream = null;
                    try {
                        inputStream = new FileInputStream(tempCentralBytesFile);
                        final byte[] buffer = new byte[1024];

                        int len;
                        while ((len = inputStream.read(buffer)) > 0){
                            fIn.write(buffer, 0, len);
                        }
                    } finally {
                        if (inputStream != null) {
                            inputStream.close();
                        }
                        tempCentralBytesFile.delete();
                    }
                } else {
                    // 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

                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);
                temp.putInt((int) (centralDirStartOffset + length + 8 - (centralDirStartOffset - apkSigningBlockOffset)));
                // 8 = size of block in bytes (excluding this field) (uint64)
                temp.flip();
                fIn.write(temp.array());

            }
        } finally {
            if (fileChannel != null) {
                fileChannel.close();
            }
            if (fIn != null) {
                fIn.close();
            }
        }
    }
  • 从这里更能看到设计接口的意图,我认为设计者可能是想把构建的逻辑抽出去,不行再放在这个长方法中了
  • 可以看到lowMemory这个参数的使用就是读写中央目录的时候,如果这个标记为为true,那就用一个临时文件储存这部分并恢复,如果为false就直接载入内存中
  • 但是这里好像没有解决问题,那就是其实它在写入的时候是吧Signing Block的偏移位置写入而不是把中央目录的偏移量写入,为什么呢?其实就是因为Android系统在校验APK的数据摘要时,首先会把EOCD的中央目录偏移量替换成签名块的偏移量,然后再计算数据摘要