關於做者:
李濤,騰訊Android工程師,14年加入騰訊SNG增值產品部,期間主要負責手Q動漫、企鵝電競等項目的功能開發和技術優化。業務時間喜歡折騰新技術,寫一些技術文章,我的技術博客:www.ltlovezh.com 。java
ApkChannelPackage是一種快速多渠道打包工具,同時支持基於V1和V2簽名進行渠道打包。插件自己會自動檢測Apk使用的簽名方法,並選擇合適的多渠道打包方式,對使用者來講徹底透明。android
Github地址:
https://github.com/ltlovezh/ApkChannelPackagegit
衆所周知,由於國內Android應用分發市場的現狀,咱們在發佈APP時,通常須要生成多個渠道包,上傳到不一樣的應用市場。這些渠道包須要包含不一樣的渠道信息,在APP和後臺交互或者數據上報時,會帶上各自的渠道信息。這樣,咱們就能統計到每一個分發市場的下載數、用戶數等關鍵數據。github
既然咱們須要進行多渠道打包,那咱們就看下最多見的多渠道打包方案。算法
Gradle Plugin自己提供了多渠道的打包策略:安全
首先,在AndroidManifest.xml中添加渠道信息佔位符:微信
<meta-data android:name="InstallChannel" android:value="${InstallChannel}" />
而後,經過Gradle Plugin提供的productFlavors
標籤,添加渠道信息:app
productFlavors{ "YingYongBao"{ manifestPlaceholders = [InstallChannel : "YingYongBao"] } "360"{ manifestPlaceholders = [InstallChannel : "360"] } }
這樣,Gradle編譯生成多渠道包時,會用不一樣的渠道信息替換AndroidManifest.xml中的佔位符。咱們在代碼中,也就能夠直接讀取AndroidManifest.xml中的渠道信息了。編輯器
可是,這種方式存在一些缺點:工具
每生成一個渠道包,都要從新執行一遍構建流程,效率過低,只適用於渠道較少的場景。
Gradle會爲每一個渠道包生成一個不一樣的BuildConfig.java類,記錄渠道信息,致使每一個渠道包的DEX的CRC值都不一樣。通常狀況下,這是沒有影響的。可是若是你使用了微信的Tinker熱補丁方案,那麼就須要爲不一樣的渠道包打不一樣的補丁,這徹底是不能夠接受的。(由於Tinker是經過對比基礎包APK和新包APK生成差分補丁,而後再把補丁和基礎包APK一塊兒合成新包APK。這就要求用於生成差分補丁的基礎包DEX和用於合成新包的基礎包DEX是徹底一致的,即:每個基礎渠道包的DEX文件是徹底一致的,否則就會合成失敗)
ApkTool是一個逆向分析工具,能夠把APK解開,添加代碼後,從新打包成APK。所以,基於ApkTool的多渠道打包方案分爲如下幾步:
通過測試,這種方案徹底是可行的。
優勢:
不須要從新構建新渠道包,僅須要複製修改就能夠了。而且由於是從新簽名,因此同時支持V1和V2簽名。
缺點:
那有沒有一種方案,能夠在添加渠道信息後,不須要從新簽名那?
首先咱們要了解一下APK的簽名和校驗機制。
在進一步學習V1和V2簽名以前,咱們有必要學習一下簽名相關的基礎知識。
數據摘要算法是一種能產生特定輸出格式的算法,其原理是根據必定的運算規則對原始數據進行某種形式的信息提取,被提取出的信息就是原始數據的消息摘要,也稱爲數據指紋。
通常狀況下,數據摘要算法具備如下特色:
不管輸入數據有多大(長),計算出來的數據摘要的長度老是固定的。例如:MD5算法計算出的數據摘要有128Bit。
通常狀況下(不考慮碰撞的狀況下),只要原始數據不一樣,那麼其對應的數據摘要就不會相同。同時,只要原始數據有任何改動,那麼其數據摘要也會徹底不一樣。即:相同的原始數據必有相同的數據摘要,不一樣的原始數據,其數據摘要也必然不一樣。
不可逆性,即只能正向提取原始數據的數據摘要,而沒法從數據摘要中恢復出原始數據。
著名的摘要算法有RSA公司的MD5算法和SHA系列算法。
數字簽名和數字證書是成對出現的,二者不可分離(數字簽名主要用來校驗數據的完整性,數字證書主要用來確保公鑰的安全發放)。
要明白數字簽名的概念,必需要了解數據的加密、傳輸和校驗流程。通常狀況下,要實現數據的可靠通訊,須要解決如下兩個問題:
而數字簽名,就是爲了解決這兩個問題而誕生的。
首先,數據的發送者須要先申請一對公私鑰對,並將公鑰交給數據接收者。
而後,若數據發送者須要發送數據給接收者,則首先要根據原始數據,生成一份數字簽名,而後把原始數據和數字簽名一塊兒發送給接收者。
數字簽名由如下兩步計算得來:
這樣,數據接收者拿到的消息就包含了兩塊內容:
接下來,接收者就會經過如下幾步,校驗數據的真實性:
由於私鑰只有發送者纔有,因此其餘人沒法僞造數字簽名。這樣經過數字簽名就確保了數據的可靠傳輸。
綜上所述,數字簽名就是只有發送者才能產生的別人沒法僞造的一段數字串,這段數字串同時也是對發送者發送數據真實性的一個有效證實。
想法雖好,可是上面的整個流程,有一個前提,就是數據接收者可以正確拿到發送者的公鑰。若是接收者拿到的公鑰被篡改了,那麼壞人就會被當成好人,而真正的數據發送者發送的數據則會被視做髒數據。那怎麼才能保證公鑰的安全性那?這就要靠數字證書來解決了。
數字證書是由有公信力的證書中心(CA)頒發給申請者的證書,主要包含了:證書的發佈機構、證書的有效期、申請者的公鑰、申請者信息、數字簽名使用的算法,以及證書內容的數字簽名。
可見,數字證書也用到了數字簽名技術。只不過簽名的內容是數據發送方的公鑰,以及一些其它證書信息。
這樣數據發送者發送的消息就包含了三部份內容:
接收者拿到數據後,首先會根據CA的公鑰,解碼出發送者的公鑰。而後就與上面的校驗流程徹底相同了。
因此,數字證書主要解決了公鑰的安全發放問題。
所以,包含數字證書的整個簽名和校驗流程以下圖所示:
默認狀況下,APK使用的就是V1簽名。解壓APK後,在META-INF
目錄下,能夠看到三個文件:MANIFEST.MF、CERT.SF、CERT.RSA。它們都是V1簽名的產物。其中,MANIFEST.MF
文件內容以下所示:
它記錄了APK中全部原始文件的數據摘要的Base64編碼,而數據摘要算法就是SHA1
。CERT.SF
文件內容以下所示:
SHA1-Digest-Manifest-Main-Attributes
主屬性記錄了MANIFEST.MF
文件全部主屬性的數據摘要的Base64編碼。SHA1-Digest-Manifest
則記錄了整個MANIFEST.MF
文件的數據摘要的Base64編碼。
其他的普通屬性則和MANIFEST.MF中的屬性一一對應,分別記錄了對應數據塊的數據摘要的Base64編碼。例如:CERT.SF
文件中skin_drawable_btm_line.xml對應的SHA1-Digest,就是下面內容的數據摘要的Base64編碼。
Name: res/drawable/skin_drawable_btm_line.xml SHA1-Digest: JqJbk6/AsWZMcGVehCXb33Cdtrk= \r\n
這裏要注意的是:最後一行的換行符是必不可少,須要參與計算的。
CERT.RSA
文件包含了對CERT.SF
文件的數字簽名和開發者的數字證書。RSA
就是計算數字簽名使用的非對稱加密算法。
V1簽名的詳細流程可參考SignApk.java,整個簽名流程以下圖所示:
整個簽名機制的最終產物就是MANIFEST.MF、CERT.SF、CERT.RSA三個文件。
在安裝APK時,Android系統會校驗簽名,檢查APK是否被篡改。代碼流程是:PackageManagerService.java
-> PackageParser.java
,PackageParser
類負責V1簽名的具體校驗。整個校驗流程以下圖所示:
若中間任何一步校驗失敗,APK就不能安裝。
OK,瞭解了V1的簽名和校驗流程。咱們來看下,V1簽名是怎麼保證APK文件不被篡改的?
首先,若是破壞者修改了APK中的任何文件,那麼被篡改文件的數據摘要的Base64編碼就和MANIFEST.MF
文件的記錄值不一致,致使校驗失敗。
其次,若是破壞者同時修改了對應文件在MANIFEST.MF
文件中的Base64值,那麼MANIFEST.MF中對應數據塊的Base64值就和CERT.SF
文件中的記錄值不一致,致使校驗失敗。
最後,若是破壞者更進一步,同時修改了對應文件在CERT.SF
文件中的Base64值,那麼CERT.SF
的數字簽名就和CERT.RSA
記錄的簽名不一致,也會校驗失敗。
那有沒有可能繼續僞造CERT.SF
的數字簽名那?理論上不可能,由於破壞者沒有開發者的私鑰。那破壞者是否是能夠用本身的私鑰和數字證書從新簽名那,這卻是徹底能夠!
綜上所述,任何對APK文件的修改,在安裝時都會失敗,除非對APK從新簽名。可是相同包名,不一樣簽名的APK也是不能同時安裝的。
由上述V1簽名和校驗機制可知,修改APK中的任何文件都會致使安裝失敗!那怎麼添加渠道信息那?只能從APK的結構入手了。
APK文件本質上是一個ZIP壓縮包,而ZIP格式是固定的,主要由三部分構成,以下圖所示:
第一部分是內容塊,全部的壓縮文件都在這部分。每一個壓縮文件都有一個local file header
,主要記錄了文件名、壓縮算法、壓縮先後的文件大小、修改時間、CRC32值等。
第二部分稱爲中央目錄,包含了多個central directory file header
(和第一部分的local file header
一一對應),每一箇中央目錄文件頭主要記錄了壓縮算法、註釋信息、對應local file header
的偏移量等,方便快速定位數據。
最後一部分是EOCD,主要記錄了中央目錄大小、偏移量和ZIP註釋信息等,其詳細結構以下圖所示:
根據以前的V1簽名和校驗機制可知,V1簽名只會檢驗第一部分的全部壓縮文件,而不理會後兩部份內容。所以,只要把渠道信息寫入到後兩塊內容就能夠經過V1校驗,而EOCD的註釋字段無疑是最好的選擇。
既然找到了突破口,那麼基於V1簽名的多渠道打包方案就應運而生:在APK文件的註釋字段,添加渠道信息。
整個方案包括如下幾步:
這裏添加魔數的好處是方便從後向前讀取數據,定位渠道信息。
所以,讀取渠道信息包括如下幾步:
經過16進制編輯器,能夠查看到添加渠道信息後的APK(小端模式),以下所示:
6C 74 6C 6F 76 75 7A 68
是魔數,04 00
表示渠道信息長度爲4,6C 65 6F 6E
就是渠道信息leon
了。0E 00
就是APK註釋長度了,正好是15。
雖然說整個方案很清晰,可是在找到EOCD數據塊
這步遇到一個問題。若是APK自己沒有註釋,那最後22字節就是EOCD。可是若APK自己已經包含了註釋字段,那怎麼肯定EOCD的起始位置那?這裏借鑑了系統V2簽名肯定EOCD位置的方案。整個計算流程以下圖所示:
整個方案介紹完了,該方案的最大優勢就是:不須要解壓縮APK,不須要從新簽名,只須要複製APK,在註釋字段添加渠道信息。每一個渠道包僅需幾秒的耗時,很是適合渠道較多的APK。
可是好景不長,Android7.0以後新增了V2簽名,該簽名會校驗整個APK的數據摘要,致使上述渠道打包方案失效。因此若是想繼續使用上述方案,須要關閉Gradle Plugin中的V2簽名選項,禁用V2簽名。
從前面的V1簽名介紹,能夠知道V1存在兩個弊端:
MANIFEST.MF
中的數據摘要是基於原始未壓縮文件計算的。所以在校驗時,須要先解壓出原始文件,才能進行校驗。而解壓操做無疑是耗時的。
V1簽名僅僅校驗APK第一部分中的文件,缺乏對APK的完整性校驗。所以,在簽名後,咱們還能夠修改APK文件,例如:經過zipalign進行字節對齊後,仍然能夠正常安裝。
正是基於這兩點,Google提出了V2簽名,解決了上述兩個問題:
V2簽名是對APK自己進行數據摘要計算,不存在解壓APK的操做,減小了校驗時間。
V2簽名是針對整個APK進行校驗(不包含簽名塊自己),所以對APK的任何修改(包括添加註釋、zipalign字節對齊)都沒法經過V2簽名的校驗。
關於第一點的耗時問題,這裏有一份實驗室數據(Nexus 6P、Android 7.1.1)可供參考。
APK安裝耗時對比 | 取5次平均耗時(秒) |
---|---|
V1簽名APK | 11.64 |
V2簽名APK | 4.42 |
可見,V2簽名對APK的安裝速度仍是提高很多的。
不一樣於V1,V2簽名會生成一個簽名塊,插入到APK中。所以,V2簽名後的APK結構以下圖所示:
APK簽名塊位於中央目錄以前,文件數據以後。V2簽名同時修改了EOCD中的中央目錄的偏移量,使簽名後的APK還符合ZIP結構。
APK簽名塊的具體結構以下圖所示:
首先是8字節的簽名塊大小,此大小不包含該字段自己的8字節;其次就是ID-Value序列,就是一個4字節的ID和對應的數據;而後又是一個8字節的簽名塊大小,與開始的8字節是相等的;最後是16字節的簽名塊魔數。
其中,ID爲0x7109871a
對應的Value就是V2簽名塊數據。
V2簽名塊的生成可參考ApkSignerV2,總體結構和流程以下圖所示:
首先,根據多個簽名算法,計算出整個APK的數據摘要,組成左上角的APK數據摘要集;
接着,把最左側一列的數據摘要
、數字證書
和額外屬性
組裝起來,造成相似於V1簽名的「MF」文件(第二列第一行);
其次,再用相同的私鑰,不一樣的簽名算法,計算出「MF」文件的數字簽名,造成相似於V1簽名的「SF」文件(第二列第二行);
而後,把第二列的相似MF文件
、相似SF文件
和開發者公鑰
一塊兒組裝成經過單個keystore簽名後的v2簽名塊(第三列第一行)。
最後,把多個keystore簽名後的簽名塊組裝起來,就是完整的V2簽名塊了(Android中容許使用多個keystore對apk進行簽名)。
上述流程比較繁瑣。簡而言之,單個keystore簽名塊主要由三部分組成,分別是上圖中第二列的三個數據塊:相似MF文件
、相似SF文件
和開發者公鑰
,其結構以下圖所示:
除此以外,Google也優化了計算數據摘要的算法,使得能夠並行計算,以下圖所示:
數據摘要的計算包括如下幾步:
這樣,每一個數據塊的數據摘要就能夠並行計算,加快了V2簽名和校驗的速度。
Android Gradle Plugin2.2之上默認會同時開啓V1和V2簽名,同時包含V1和V2簽名的CERT.SF文件會有一個特殊的主屬性,以下圖所示:
該屬性會強制APK走V2校驗流程(7.0之上),以充分利用V2簽名的優點(速度快和更完善的校驗機制)。
所以,同時包含V1和V2簽名的APK的校驗流程以下所示:
簡而言之:優先校驗V2,沒有或者不認識V2,則校驗V1。
這裏引伸出另一個問題:APK簽名時,只有V2簽名,沒有V1簽名行不行?
通過嘗試,這種狀況是能夠編譯經過的,而且在Android 7.0之上也能夠正確安裝和運行。可是7.0之下,由於不認識V2,又沒有V1簽名,因此會報沒有簽名的錯誤。
OK,明確了Android平臺對V1和V2簽名的校驗選擇以後,咱們來看下V2簽名的具體校驗流程(PackageManagerService.java
-> PackageParser.java
-> ApkSignatureSchemeV2Verifier.java
),以下圖所示:
其中,最強簽名算法是根據該算法使用的數據摘要算法來對比產生的,好比:SHA512 > SHA256。
校驗成功的定義是至少找到一個keystore對應的簽名塊,而且全部簽名塊都按照上述流程校驗成功。
下面咱們來看下V2簽名是怎麼保證APK不被篡改的?
首先,若是破壞者修改了APK文件的任何部分(簽名塊自己除外),那麼APK的數據摘要就和「MF」數據塊中記錄的數據摘要不一致,致使校驗失敗。
其次,若是破壞者同時修改了「MF」數據塊中的數據摘要,那麼「MF」數據塊的數字簽名就和「SF」數據塊中記錄的數字簽名不一致,致使校驗失敗。
而後,若是破壞者使用本身的私鑰去加密生成「SF」數據塊,那麼使用開發者的公鑰去解密「SF」數據塊中的數字簽名就會失敗;
最後,更進一步,若破壞者甚至替換了開發者公鑰,那麼使用數字證書中的公鑰校驗簽名塊中的公鑰就會失敗,這也正是數字證書的做用。
綜上所述,任何對APK的修改,在安裝時都會失敗,除非對APK從新簽名。可是相同包名,不一樣簽名的APK也是不能同時安裝的。
到這裏,V2簽名已經介紹完了。可是在最後一步「數據摘要校驗」這裏,隱藏了一個點,不知道有沒有人發現?
由於,咱們V2簽名塊中的數據摘要是針對APK的文件內容塊、中央目錄和EOCD三塊內容計算的。可是在寫入簽名塊後,修改了EOCD中的中央目錄偏移量,那麼在進行V2簽名校驗時,理論上在「數據摘要校驗」這步應該會校驗失敗啊!可是爲何V2簽名能夠校驗經過那?
這個問題很重要,由於咱們下面要介紹的基於V2簽名的多渠道打包方案也會修改EOCD的中央目錄偏移量。
其實也很簡單,原來Android系統在校驗APK的數據摘要時,首先會把EOCD的中央目錄偏移量替換成簽名塊的偏移量,而後再計算數據摘要。而簽名塊的偏移量不就是v2簽名以前的中央目錄偏移量嘛!!!,所以,這樣計算出的數據摘要就和「MF」數據塊中的數據摘要徹底一致了。具體代碼邏輯,可參考ApkSignatureSchemeV2Verifier.java的416 ~ 420行。
在上節V2簽名的校驗流程中,有一個很重要的細節:Android系統只會關注ID爲0x7109871a的V2簽名塊,而且忽略其餘的ID-Value,同時V2簽名只會保護APK自己,不包含簽名塊。
所以,基於V2簽名的多渠道打包方案就應運而生:在APK簽名塊中添加一個ID-Value,存儲渠道信息。
整個方案包括如下幾步:
實際上,除了渠道信息,咱們能夠在APK簽名塊中添加任何輔助信息。
經過16進制編輯器,能夠查看到添加渠道信息後的APK(小端模式),以下所示:
6C 65 6F 6E
就是咱們的渠道信息leon
。向前4個字節:FF 55 11 88
就是咱們添加的ID,再向前8個字節:08 00 00 00 00 00 00 00
就是咱們的ID-Value的長度,正好是8。
整個方案介紹完了,該方案的最大優勢就是:支持7.0之上新增的V2簽名,同時兼有V1方案的全部優勢。
那麼如何保證經過這些方案生成的渠道包,可以在全部Android平臺上正確安裝那?
原來Google提供了一個同時支持V1和V2簽名和校驗的工具:apksig。它包括一個apksigner
命令行和一個apksig
類庫。其中前者就是Android SDK build-tools下面的命令行工具。而咱們正是藉助後面的apksig來進行渠道包強校驗,它能夠保證渠道包在apk Minsdk ~ 最高版本之間都校驗經過。詳細代碼可參考VerifyApk.java
目前市面上的多渠道打包工具主要有packer-ng-plugin和美團的Walle。下表是咱們的ApkChannelPackage和它們之間的簡單對比。
這裏我之因此同時支持V1和V2簽名方案,主要是擔憂後續Android平臺增強簽名校驗機制,致使V2多渠道打包方案行不通,能夠無痛切換到V1簽名方案。後續我也會盡快支持命令行工具。
目前Gradle Plugin 2.2以上默認開啓V2簽名,因此若是想關閉V2簽名,可將下面的v2SigningEnabled
設置爲false。
signingConfigs { release { ... v1SigningEnabled true v2SigningEnabled false } debug { ... v1SigningEnabled true v2SigningEnabled false } }
1.在根工程的build.gradle
中,添加對打包Plugin的依賴:
dependencies { classpath 'com.android.tools.build:gradle:2.2.0' classpath 'com.leon.channel:plugin:1.0.1'}
2.在主App工程的build.gradle
中,添加對ApkChannelPackage Plugin的引用:
apply plugin: 'channel'
3.在主App工程的build.gradle
中,添加讀取渠道信息的helper類庫依賴:
dependencies { compile 'com.leon.channel:helper:1.0.1'}
4.在gradle.properties文件中,配置渠道文件名稱
channel_file=channel.txt
其中channel.txt即爲包含渠道信息的文件,需放置在根工程目錄下,一行一個渠道信息。
5.渠道包信息配置
如果直接編譯生成多渠道包,則經過channel標籤配置:
channel{ //多渠道包的輸出目錄,默認爲new File(project.buildDir,"channel") baseOutputDir = new File(project.buildDir,"xxx") //多渠道包的命名規則,默認爲:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType} apkNameFormat ='${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'}
其中,多渠道包的命名規則中,可以使用如下字段:
如果根據已有基礎包生成多渠道包,則經過rebuildChannel
標籤配置:
rebuildChannel { baseDebugApk = 已有Debug APK baseReleaseApk = 已有Release APK//默認爲new File(project.buildDir, "rebuildChannel/debug")debugOutputDir = Debug渠道包輸出目錄 //默認爲new File(project.buildDir, "rebuildChannel/release")releaseOutputDir = Release渠道包輸出目錄 }
這裏要注意一下,已有APK的名字必須包含base
字符串,這樣插件生成多渠道包時,會用當前的渠道替換base
字符串,造成新的渠道包。
6.生成多渠道包
若沒有經過Gradle Plugin的 productFlavors
配置多渠道,那麼經過如下Task
channelDebug
、channelRelease
分別負責生成Debug和Release的多渠道包。
如果配置了productFlavors
,那麼對應的Task則是channelFlavorXDebug
、channelFlavorXRelease
,FlavorX表示在productFlavors
中配置的渠道名稱。
除此以外,若是是根據已有基礎包生成多渠道包,那麼對應的Task則是reBuildChannel
。
7.讀取渠道信息
經過helper類庫中的ChannelReaderUtil
類讀取渠道信息。
String channel = ChannelReaderUtil.getChannel(getApplicationContext());
更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!