本文已在個人公衆號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