本文已在个人公众号hongyangAndroid原创发布。
html
你们应该都清楚,你们上线app,须要上线各类平台,好比:小米,华为,百度等等等等,咱们多数称之为渠道,若是发的渠道多,可能有上百个渠道。java
针对每一个渠道,咱们但愿能够获取各个渠道的一些独立的统计信息,好比:下载量等。android
那么,如何区分各个渠道呢?git
咱们须要一个特性的标识符与该渠道对应,这个标识符确定是要包含在apk中的。那么,咱们就要针对每一个渠道包去设置一个特定的标识符,而后打一个特定的apk。github
这个过程能够手动去完成,每次修改一个字符串,而后手动打包。你们都清楚打包是一个至关耗时的过程,要是打几百个渠道包,这种枯燥重复的任务,固然不是咱们所能容忍的。数据库
固然,咱们会想到,这样的需求,官方确定有解决方案。没错,Gradle Plugin为咱们提供了一个自动化的方案,咱们能够利用占位符,而后在build.gradle中去配置多个渠道信息,这样就能够将枯燥重复的任务自动化了。api
这样的方式最大的问题,就是效率问题,每一个渠道包,都要执行一遍构建流程。安全
自动化了,时间依然过长,仍是不能忍。bash
接下来就是寻找高效率的方案了。微信
由于本文是源码解析,就不饶弯子了~~
目前针对 V1(Android N开始推出了V2),快速的方案,主要有:
主要利用修改apk的目录META-INF中添加空文件,因为不须要从新签名,操做很是快。
利用zip文件中的comment的字段,例如VasDolly
后面在解析源码时,会详细说明方式2。
自Android N以后,Google建议使用V2来作签名,由于这样更加安全(对整个apk文件进行hash校验,没法修改apk信息),安装速度也更加高效(无需解析校验单个文件,v1须要单个文件校验hash)。
美团对此动做很是快,立马推出了:
其原理是利用v2的方式在作签名时,在apk中插入了一个签名块(安装时校验apk的hash不包含此块),该快中容许插入一些key-value对,因而将签名插在该区域。
固然,腾讯的VasDolly采起的也是相同的方案。
本文,为VasDolly的源码解析,即会详细分析:
本文不涉及v1,v2具体的签名方式,以及安装时的校验流程,这些内容在:
一文中,说的很是详细。
本文重点是源码的解析。
其实,接入很是简单,并且readme写的很是详细。
可是为了文章的完整性,简单陈述一下。
buildscript {
dependencies {
classpath 'com.leon.channel:plugin:1.1.7'
}
}
复制代码
apply plugin: 'channel'
android {
signingConfigs {
release {
storeFile file(RELEASE_STORE_FILE) storePassword RELEASE_STORE_PASSWORD keyAlias RELEASE_KEY_ALIAS keyPassword RELEASE_KEY_PASSWORD v1SigningEnabled true v2SigningEnabled false } } buildTypes {
release {
signingConfig signingConfigs.release minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } channel{
//指定渠道文件
channelFile = file("/Users/zhanghongyang01/git-repo/learn/VasDollyTest/channel.txt")
//多渠道包的输出目录,默认为new File(project.buildDir,"channel")
baseOutputDir = new File(project.buildDir,"channel")
//多渠道包的命名规则,默认为:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}
apkNameFormat ='${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'
//快速模式:生成渠道包时不进行校验(速度能够提高10倍以上)
isFastMode = true
}
}
dependencies {
api 'com.leon.channel:helper:1.1.7'
}
复制代码
首先要apply plugin,而后在android的闭包下写入channel相关信息。
channel中须要制定一个channel.txt文件,其中每行代码一个渠道:
c1
c2
c3
复制代码
dependencies中的依赖主要是为了获取渠道号的辅助类,毕竟你写入渠道信息的地方这么奇怪,确定要提供API进行读取渠道号。
注意:咱们在signingConfigs的release中配置的是:v1SigningEnabled=true
和v2SigningEnabled=false
,先看V1方式的快速渠道包。
在Terminal面板执行./gradlew channelRelease
执行完成后,便可在app/build/channel/release
下看到:
release
├── app-1.0-1-c1-release.apk
├── app-1.0-1-c2-release.apk
└── app-1.0-1-c3-release.apk
复制代码
注意:本文主要用于讲解源码,若是只需接入,尽量查看github文档。
首先咱们须要知道对于V1的签名,渠道信息写在哪?
这里直接白话说明一下,咱们的apk实际上就是普通的zip,在一个zip文件的最后容许写入N个字符的注释,咱们关注的zip末尾两个部分:
2字节的的注释长度+N个字节的注释。
那么,咱们只要把签名内容做为注释写入,再修改2字节的注释长度便可。
如今须要考虑的是咱们怎么知道一个apk有没有写入这个渠道信息呢,须要有一个判断的标准:
这时候,魔数这个概念产生了,咱们能够在文件文件末尾写入一个特殊的字符串,当咱们读取文件末尾为这个特殊的字符串,便可认为该apk写入了渠道信息。
不少文件类型起始部分都包含特性的魔数用于区分文件类型。
最终的渠道信息为:
渠道字符串+渠道字符串长度+魔数
有了上面的分析,读取就简单了:
在看源码以前,咱们也可使用二进制编辑器打开打包好的Apk,看末尾的几个字节,如图:
我们逆着看:
这样咱们就读取除了渠道信息为:c1。
这么看代码也不复杂,最后看一眼代码吧:
代码中经过ChannelReaderUtil.getChannel获取渠道信息:
public static String getChannel(Context context) {
if (mChannelCache == null) {
String channel = getChannelByV2(context);
if (channel == null) {
channel = getChannelByV1(context);
}
mChannelCache = channel;
}
return mChannelCache;
}
复制代码
咱们只看v1,根据调用流程,最终会到:
V1SchemeUtil.readChannel方法:
public static String readChannel(File file) throws Exception {
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, "r");
long index = raf.length();
byte[] buffer = new byte[ChannelConstants.V1_MAGIC.length];
index -= ChannelConstants.V1_MAGIC.length;
raf.seek(index);
raf.readFully(buffer);
// whether magic bytes matched
if (isV1MagicMatch(buffer)) {
index -= ChannelConstants.SHORT_LENGTH;
raf.seek(index);
// read channel length field
int length = readShort(raf);
if (length > 0) {
index -= length;
raf.seek(index);
// read channel bytes
byte[] bytesComment = new byte[length];
raf.readFully(bytesComment);
return new String(bytesComment, ChannelConstants.CONTENT_CHARSET);
} else {
throw new Exception("zip channel info not found");
}
} else {
throw new Exception("zip v1 magic not found");
}
} finally {
if (raf != null) {
raf.close();
}
}
}
复制代码
使用了RandomAccessFile,能够很方便的使用seek指定到具体的字节处。注意第一次seek的目标是length - magic.length
,即对应咱们的读取魔数,读取到比对是否相同。
若是相同,再往前读取SHORT_LENGTH = 2
个字节,读取为short类型,即为渠道信息所占据的字节数。
再往前对去对应的长度,转化为String,即为渠道信息,与咱们前面的分析如出一辙。
ok,读取始终是简单的。
后面还要看如何写入以及如何自动化。
写入渠道信息,先思考下,有个apk,须要写入渠道信息,须要几步:
好像惟一的难点就是找到合适的位置。
可是找到这个合适的位置,又涉及到zip文件的格式内容了。
大体讲解下:
zip的末尾有一个数据库,这个数据块咱们叫作EOCD块,分为4个部分:
知道这个规律后,咱们就能够经过匹配1中固定值来肯定对应区域,而后seek到注释处。
可能99.99%的apk默认是不包含注释内容的,因此直接往前seek 22个字节,读取4个字节作下匹配便可。
可是若是已经包含了注释内容,就比较难办了。不少时候,咱们会正向从头开始按协议读取zip文件格式,直至到达目标区域。
不过VasDolly的作法是,从文件末尾seek 22 ~ 文件size - 22,逐一匹配。
咱们简单看下代码:
public static void writeChannel(File file, String channel) throws Exception {
byte[] comment = channel.getBytes(ChannelConstants.CONTENT_CHARSET);
Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(file);
if (eocdAndOffsetInFile.getFirst().remaining() == ZipUtils.ZIP_EOCD_REC_MIN_SIZE) {
System.out.println("file : " + file.getAbsolutePath() + " , has no comment");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
//1.locate comment length field
raf.seek(file.length() - ChannelConstants.SHORT_LENGTH);
//2.write zip comment length (content field length + length field length + magic field length)
writeShort(comment.length + ChannelConstants.SHORT_LENGTH + ChannelConstants.V1_MAGIC.length, raf);
//3.write content
raf.write(comment);
//4.write content length
writeShort(comment.length, raf);
//5. write magic bytes
raf.write(ChannelConstants.V1_MAGIC);
raf.close();
} else {
System.out.println("file : " + file.getAbsolutePath() + " , has comment");
if (containV1Magic(file)) {
try {
String existChannel = readChannel(file);
if (existChannel != null){
file.delete();
throw new ChannelExistException("file : " + file.getAbsolutePath() + " has a channel : " + existChannel + ", only ignore");
}
}catch (Exception e){
e.printStackTrace();
}
}
int existCommentLength = ZipUtils.getUnsignedInt16(eocdAndOffsetInFile.getFirst(), ZipUtils.ZIP_EOCD_REC_MIN_SIZE - ChannelConstants.SHORT_LENGTH);
int newCommentLength = existCommentLength + comment.length + ChannelConstants.SHORT_LENGTH + ChannelConstants.V1_MAGIC.length;
RandomAccessFile raf = new RandomAccessFile(file, "rw");
//1.locate comment length field
raf.seek(eocdAndOffsetInFile.getSecond() + ZipUtils.ZIP_EOCD_REC_MIN_SIZE - ChannelConstants.SHORT_LENGTH);
//2.write zip comment length (existCommentLength + content field length + length field length + magic field length)
writeShort(newCommentLength, raf);
//3.locate where channel should begin
raf.seek(eocdAndOffsetInFile.getSecond() + ZipUtils.ZIP_EOCD_REC_MIN_SIZE + existCommentLength);
//4.write content
raf.write(comment);
//5.write content length
writeShort(comment.length, raf);
//6.write magic bytes
raf.write(ChannelConstants.V1_MAGIC);
raf.close();
}
}
复制代码
getEocd(file)的的返回值是Pair<ByteBuffer, Long>
,多数状况下first为EOCD块起始位置到结束后的内容;second为EOCD块起始位置。
if为apk自己无comment的状况,这种方式属于大多数状况,从文件末尾,移动2字节,该2字节为注释长度,而后组装注释内容,从新计算注释长度,从新写入注释长度,再写入注释内容,最后写入MAGIC魔数。
else即为自己存在comment的状况,首先读取原有注释长度,而后根据渠道等信息计算出先的注释长度,写入。
最后咱们看下,是如何作到输入./gradle channelRelease
就实现全部渠道包的生成呢。
这里主要就是解析gradle plugin了,若是你尚未自定义过plugin,很是值得参考。
代码主要在VasDolly/plugin这个module.
入口代码为ApkChannelPackagePlugin的apply方法。
主要代码:
project.afterEvaluate {
project.android.applicationVariants.all { variant ->
def variantOutput = variant.outputs.first();
def dirName = variant.dirName;
def variantName = variant.name.capitalize();
Task channelTask = project.task("channel${variantName}", type: ApkChannelPackageTask) {
mVariant = variant;
mChannelExtension = mChannelConfigurationExtension;
mOutputDir = new File(mChannelConfigurationExtension.baseOutputDir, dirName)
mChannelList = mChanneInfolList
dependsOn variant.assemble
}
}
}
复制代码
为每一个variantName添加了一个task,而且依赖于variant.assemble
。
也就是说,当咱们执行./gradlew channelRelease
时,会先执行assemble,而后对产物apk作后续操做。
重点看这个Task,ApkChannelPackageTask
。
执行代码为:
@TaskAction
public void channel() {
//1.check all params
checkParameter();
//2.check signingConfig , determine channel package mode
checkSigningConfig()
//3.generate channel apk
generateChannelApk();
}
复制代码
注释也比较清晰,首先channelFile、baseOutputDir等相关参数。接下来校验signingConfig中v2SigningEnabled与v1SigningEnabled,肯定使用V1仍是V2 mode,咱们上文中将v2SigningEnabled设置为了false,因此这里为V1_MODE。
最后就是生成渠道apk了:
void generateV1ChannelApk() {
// 省略了一些代码
mChannelList.each { channel ->
String apkChannelName = getChannelApkName(channel)
println "generateV1ChannelApk , channel = ${channel} , apkChannelName = ${apkChannelName}"
File destFile = new File(mOutputDir, apkChannelName)
copyTo(mBaseApk, destFile)
V1SchemeUtil.writeChannel(destFile, channel)
if (!mChannelExtension.isFastMode){
//1. verify channel info
if (V1SchemeUtil.verifyChannel(destFile, channel)) {
println("generateV1ChannelApk , ${destFile} add channel success")
} else {
throw new GradleException("generateV1ChannelApk , ${destFile} add channel failure")
}
//2. verify v1 signature
if (VerifyApk.verifyV1Signature(destFile)) {
println "generateV1ChannelApk , after add channel , apk ${destFile} v1 verify success"
} else {
throw new GradleException("generateV1ChannelApk , after add channel , apk ${destFile} v1 verify failure")
}
}
}
println("------ ${project.name}:${name} generate v1 channel apk , end ------")
}
复制代码
很简单,遍历channelList,而后调用V1SchemeUtil.writeChannel
,该方法即咱们上文解析过的方法。
若是fastMode设置为false,还会读取出渠道再作一次强校验;以及会经过apksig作对签名进行校验。
ok,到这里咱们就彻底剖析了基于V1的快速签名的全过程。
接下来咱们看基于v2的快速签名方案。
关于V2签名的产生缘由,原理以及安装时的校验过程能够参考 VasDolly实现原理。
我这里就抛开细节,尽量让你们能明白整个过程,v2签名的原理能够简单理解为:
在这个签名块的某个区域,容许咱们写一些key-value对,咱们就将渠道信息写在这个地方。
这里有一个问题,v2不是说是对整个apk进行校验吗?为何还可以让咱们在apk中插入这样的信息呢?
由于在校验过程当中,对于签名块是不校验的(细节上因为咱们插入了签名块,某些偏移量会变化,可是在校验前,Android系统会先重置偏移量),而咱们的渠道信息恰好写在这个签名块中。
好了,细节一会看代码。
写入渠道信息,根据咱们上述的分析,流程应该大体以下:
这里咱们不按照整个代码流程走了,太长了,一会看几段关键代码。
咱们的apk如今格式是这样的:
块1+签名块+块2+块3
其中块3称之为EOCD,如今必需要展现下其内部的数据结构了:
图片来自:参考
在V1的相关代码中,咱们已经能够定位到EOCD的位置了,而后往下16个字节便可拿到Offset of start of central directory
即为块2开始的位置,也为签名块末尾的位置。
块2 再往前,就能够获取到咱们的 签名块了。
咱们先看一段代码,定位到 块2 的开始位置。
# V2SchemeUtil
public static ByteBuffer getApkSigningBlock(File channelFile) throws ApkSignatureSchemeV2Verifier.SignatureNotFoundException, IOException {
RandomAccessFile apk = new RandomAccessFile(channelFile, "r");
//1.find the EOCD
Pair<ByteBuffer, Long> eocdAndOffsetInFile = ApkSignatureSchemeV2Verifier.getEocd(apk);
ByteBuffer eocd = eocdAndOffsetInFile.getFirst();
long eocdOffset = eocdAndOffsetInFile.getSecond();
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
throw new ApkSignatureSchemeV2Verifier.SignatureNotFoundException("ZIP64 APK not supported");
}
//2.find the APK Signing Block. The block immediately precedes the Central Directory.
long centralDirOffset = ApkSignatureSchemeV2Verifier.getCentralDirOffset(eocd, eocdOffset);//经过eocd找到中央目录的偏移量
//3. find the apk V2 signature block
Pair<ByteBuffer, Long> apkSignatureBlock =
ApkSignatureSchemeV2Verifier.findApkSigningBlock(apk, centralDirOffset);//找到V2签名块的内容和偏移量
return apkSignatureBlock.getFirst();
}
复制代码
首先发现EOCD块,这个前面咱们已经分析了。
而后寻找到签名块的位置,上面咱们已经分析了只要往下移动16字节便可到达签名块末尾 ,那么看下ApkSignatureSchemeV2Verifier.getCentralDirOffset
代码,最终调用:
public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
assertByteOrderLittleEndian(zipEndOfCentralDirectory);
return getUnsignedInt32(
zipEndOfCentralDirectory,
zipEndOfCentralDirectory.position() + 16);
}
复制代码
到这里咱们已经能够到达签名块末尾了。
咱们继续看findApkSigningBlock找到V2签名块的内容和偏移量:
public static Pair<ByteBuffer, Long> findApkSigningBlock( RandomAccessFile apk, long centralDirOffset) throws IOException, SignatureNotFoundException {
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");
}
// Read and compare size fields
long apkSigBlockSizeInFooter = footer.getLong(0);
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
long apkSigBlockOffset = centralDirOffset - totalSize;
ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
apk.seek(apkSigBlockOffset);
apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity());
return Pair.create(apkSigBlock, apkSigBlockOffset);
}
复制代码
这里咱们须要介绍下签名块相关信息了:
图片来自:参考
中间的不包含此8字节,值得是该ID-VALUE的size值不包含此8字节。
首先往前读取24个字节,即读取了签名块大小64bits+魔数128bits;而后会魔数信息与实际的魔数对比。
接下来读取8个字节为apkSigBlockSizeInFooter,即签名块大小。
而后+8加上上图顶部的8个字节。
最后将整个签名块读取到ByteBuffer中返回。
此时咱们已经有了签名块的全部数据了。
接下来咱们要读取这个签名块中全部的key-value对!
# V2SchemeUtil
public static Map<Integer, ByteBuffer> getAllIdValue(ByteBuffer apkSchemeBlock) {
ApkSignatureSchemeV2Verifier.checkByteOrderLittleEndian(apkSchemeBlock);
ByteBuffer pairs = ApkSignatureSchemeV2Verifier.sliceFromTo(apkSchemeBlock, 8, apkSchemeBlock.capacity() - 24);
Map<Integer, ByteBuffer> idValues = new LinkedHashMap<Integer, ByteBuffer>(); // keep order
int entryCount = 0;
while (pairs.hasRemaining()) {
entryCount++;
long lenLong = pairs.getLong();
int len = (int) lenLong;
int nextEntryPos = pairs.position() + len;
int id = pairs.getInt();
idValues.put(id, ApkSignatureSchemeV2Verifier.getByteBuffer(pairs, len - 4));//4 is length of id
if (id == ApkSignatureSchemeV2Verifier.APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
System.out.println("find V2 signature block Id : " + ApkSignatureSchemeV2Verifier.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
}
pairs.position(nextEntryPos);
}
return idValues;
}
复制代码
首先读取8到capacity() - 24中的内容,即全部的id-value集合。
而后进入while循环,读取一个个key-value存入idValues,咱们看下循环体内:
如此循环,获得全部的idValues。
有了全部的idValues,而后根据特定的id,便可获取咱们的渠道信息了。
即:
# ChannelReader
public static String getChannel(File channelFile) {
System.out.println("try to read channel info from apk : " + channelFile.getAbsolutePath());
return IdValueReader.getStringValueById(channelFile, ChannelConstants.CHANNEL_BLOCK_ID);
}
复制代码
这样咱们就走通了读取的逻辑。
我替你们总结下:
先思考下,如今要正视的是,目前到咱们这里已是v2签名打出的包了。那么咱们应该找到签名块中的id-values部分,把咱们的渠道信息插入进去。
大体的方式能够为:
# V2SchemeUtil
public static ApkSectionInfo getApkSectionInfo(File baseApk) {
RandomAccessFile apk = new RandomAccessFile(baseApk, "r");
//1.find the EOCD and offset
Pair<ByteBuffer, Long> eocdAndOffsetInFile = ApkSignatureSchemeV2Verifier.getEocd(apk);
ByteBuffer eocd = eocdAndOffsetInFile.getFirst();
long eocdOffset = eocdAndOffsetInFile.getSecond();
//2.find the APK Signing Block. The block immediately precedes the Central Directory.
long centralDirOffset = ApkSignatureSchemeV2Verifier.getCentralDirOffset(eocd, eocdOffset);//经过eocd找到中央目录的偏移量
Pair<ByteBuffer, Long> apkSchemeV2Block =
ApkSignatureSchemeV2Verifier.findApkSigningBlock(apk, centralDirOffset);//找到V2签名块的内容和偏移量
//3.find the centralDir
Pair<ByteBuffer, Long> centralDir = findCentralDir(apk, centralDirOffset, (int) (eocdOffset - centralDirOffset));
//4.find the contentEntry
Pair<ByteBuffer, Long> contentEntry = findContentEntry(apk, (int) apkSchemeV2Block.getSecond().longValue());
ApkSectionInfo apkSectionInfo = new ApkSectionInfo();
apkSectionInfo.mContentEntry = contentEntry;
apkSectionInfo.mSchemeV2Block = apkSchemeV2Block;
apkSectionInfo.mCentralDir = centralDir;
apkSectionInfo.mEocd = eocdAndOffsetInFile;
System.out.println("baseApk : " + baseApk.getAbsolutePath() + " , ApkSectionInfo = " + apkSectionInfo);
return apkSectionInfo;
}
复制代码
所有都存储到apkSectionInfo中。
目前咱们将整个apk按区域读取出来了。
# ChannelWriter
public static void addChannel(ApkSectionInfo apkSectionInfo, File destApk, String channel) {
byte[] buffer = channel.getBytes(ChannelConstants.CONTENT_CHARSET);
ByteBuffer channelByteBuffer = ByteBuffer.wrap(buffer);
//apk中全部字节都是小端模式
channelByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
IdValueWriter.addIdValue(apkSectionInfo, destApk, ChannelConstants.CHANNEL_BLOCK_ID, channelByteBuffer);
}
复制代码
将渠道字符串与特定的渠道id准备好,调用addIdValue
# IdValueWriter
public static void addIdValue(ApkSectionInfo apkSectionInfo, File destApk, int id, ByteBuffer valueBuffer) {
Map<Integer, ByteBuffer> idValueMap = new LinkedHashMap<>();
idValueMap.put(id, valueBuffer);
addIdValueByteBufferMap(apkSectionInfo, destApk, idValueMap);
}
复制代码
继续:
public static void addIdValueByteBufferMap(ApkSectionInfo apkSectionInfo, File destApk, Map<Integer, ByteBuffer> idValueMap) {
Map<Integer, ByteBuffer> existentIdValueMap = V2SchemeUtil.getAllIdValue(apkSectionInfo.mSchemeV2Block.getFirst());
existentIdValueMap.putAll(idValueMap);
ByteBuffer newApkSigningBlock = V2SchemeUtil.generateApkSigningBlock(existentIdValueMap);
ByteBuffer contentEntry = apkSectionInfo.mContentEntry.getFirst();
ByteBuffer centralDir = apkSectionInfo.mCentralDir.getFirst();
ByteBuffer eocd = apkSectionInfo.mEocd.getFirst();
long centralDirOffset = apkSectionInfo.mCentralDir.getSecond();
//update the offset of centralDir
centralDirOffset += (newApkSigningBlock.remaining() - apkSectionInfo.mSchemeV2Block.getFirst().remaining());
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset);//修改了apkSectionInfo中eocd的原始数据
RandomAccessFile fIn = new RandomAccessFile(destApk, "rw");
long apkLength = contentEntry.remaining() + newApkSigningBlock.remaining() + centralDir.remaining() + eocd.remaining();
fIn.seek(0l);
//1. write real content Entry block
fIn.write(contentEntry.array(), contentEntry.arrayOffset() + contentEntry.position(), contentEntry.remaining());
//2. write new apk v2 scheme block
fIn.write(newApkSigningBlock.array(), newApkSigningBlock.arrayOffset() + newApkSigningBlock.position(), newApkSigningBlock.remaining());
//3. write central dir block
fIn.write(centralDir.array(), centralDir.arrayOffset() + centralDir.position(), centralDir.remaining());
//4. write eocd block
fIn.write(eocd.array(), eocd.arrayOffset() + eocd.position(), eocd.remaining());
fIn.setLength(apkLength);
System.out.println("addIdValueByteBufferMap , after add channel , new apk is " + destApk.getAbsolutePath() + " , length = " + apkLength);
}
复制代码
首先读取出本来的id-values,代码咱们前面已经分析过,与咱们要添加的id-value放到一个map中。
而后调用V2SchemeUtil.generateApkSigningBlock
从新生成一个新的签名块,这里不看了,其实就是根据上图的字节描述,很容易生成。
再根据新的签名块,和以前的中间目录偏移量,计算出新的偏移量,调整EOCD中的相关值。
最后,经过RandomAccessFile从新写入:
完工!
关于V2的gradle部分与V1部分基本一致,再也不赘述。
最后,对于文中的块1+签名块+块2+块3,主要是为了方便理解,你们能够再去了解下zip文件格式,对应到专业的术语上去。
支持个人话能够关注下个人公众号和网站,天天都会推送新知识~
扫一扫关注个人微信公众号:hongyangAndroid