美团多渠道打包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的中央目录偏移量替换成签名块的偏移量,然后再计算数据摘要