Android 熱修復方案Tinker(六) Gradle外掛實現
基於Tinker V1.7.5
這篇文章主要分析一下Tinker中gradle外掛的設計以及各個任務的職能.Gradle外掛工作流程的簡單實現在Android Gradle 外掛編寫文章中有講過,這裡就不復述了.下圖是Tinker Gradle外掛的類圖結構.點選檢視大圖
Gradle
Gradle裡需要配置外掛中自定義的擴充套件.擴充套件block層級,屬性和含義結合Tinker的文件如下.
tinkerPatch
- buildConfig
- dex
- lib
- res
- packageConfig
- sevenZip
tinkerPatch
全域性資訊相關的配置項
引數 預設值 描述 oldApk null 基準apk包的路徑,必須輸入,否則會報錯。 ignoreWarning false 如果出現以下的情況,並且ignoreWarning為false,Tinker將中斷編譯。因為這些情況可能會導致編譯出來的patch包帶來風險:
1. minSdkVersion小於14,但是dexMode
的值為”raw”;
2. 新編譯的安裝包出現新增的四大元件(Activity, BroadcastReceiver…);
3. 定義在dex.loader用於載入補丁的類不在main dex中;
4. 定義在dex.loader用於載入補丁的類出現修改;
5. resources.arsc改變,但沒有使用applyResourceMapping編譯。useSign true 在執行過程中,Tinker需要驗證基準apk包與補丁包的簽名是否一致,Tinker是否需要為你簽名。 buildConfig
編譯相關的配置項
引數 預設值 描述 applyMapping null 可選引數;在編譯新的apk時候,Tinker通過保持舊apk的proguard混淆方式,從而減少補丁包的大小。這個只是推薦的,但 設定applyMapping會影響任何的assemble編譯
。applyResourceMapping null 可選引數;在編譯新的apk時候,Tinker通過舊apk的 R.txt
可以減少補丁包的大小
,同時也避免由於ResId改變導致remote view異常
。tinkerId null 在執行過程中,Tinker需要驗證基準apk包的tinkerId是否等於補丁包的tinkerId。這個是決定補丁包能執行在哪些基準包上面,一般來說我們可以使用git版本號、versionName等等。 dex
dex相關的配置項
引數 預設值 描述 dexMode jar 只能是’raw’或者’jar’。
對於’raw’模式,Tinker將會保持輸入dex的格式。
對於’jar’模式,Tinker會把輸入dex重新壓縮封裝到jar。如果你的minSdkVersion小於14,你必須選擇‘jar’模式,而且它更省儲存空間,但是驗證md5時比’raw’模式耗時()。usePreGeneratedPatchDex flase 是否提前生成dex,而非合成的方式。這套方案即回退成Qzone的方案,對於需要使用 加固或者多flavor打包(建議使用其他方式生成渠道包)的使用者
可使用。但是這套方案需要插樁,會造成Dalvik下效能損耗以及Art補丁包可能過大的問題,務必謹慎使用
。pattern [] 需要處理dex路徑,支援*、?萬用字元,必須使用’/’分割。路徑是相對安裝包的,例如/assets/… loader [] 這一項非常重要,它定義了哪些類在載入補丁包的時候會用到。這些類是通過Tinker無法修改的類,也是一定要放在main dex的類。
這裡需要定義的類有:
1. 你自己定義的Application類;
2. Tinker庫中用於載入補丁包的部分類,即com.tencent.tinker.loader.*;
3. 如果你自定義了TinkerLoader,需要將它以及它引用的所有類也加入loader中;
4. 其他一些你不希望被更改的類,例如Sample中的BaseBuildInfo類。這裡需要注意的是,這些類的直接引用類也需要加入到loader中。或者你需要將這個類變成非preverify。lib
lib相關的配置項
引數 預設值 描述 pattern [] 需要處理lib路徑,支援*、?萬用字元,必須使用’/’分割。與dex.pattern一致, 路徑是相對安裝包的,例如/assets/… res
res相關的配置項
引數 預設值 描述 pattern [] 需要處理res路徑,支援*、?萬用字元,必須使用’/’分割。與dex.pattern一致, 路徑是相對安裝包的,例如/assets/…, 務必注意的是,只有滿足pattern的資源才會放到合成後的資源包。
ignoreChange [] 支援*、?萬用字元,必須使用’/’分割。若滿足ignoreChange的pattern,在編譯時會忽略該檔案的新增、刪除與修改。 最極端的情況,ignoreChange與上面的pattern一致,即會完全忽略所有資源的修改。 largeModSize 100 對於修改的資源,如果大於largeModSize,我們將使用bsdiff演算法。這可以降低補丁包的大小,但是會增加合成時的複雜度。預設大小為100kb packageConfig
用於生成補丁包中的’package_meta.txt’檔案
引數 預設值 描述 configField TINKER_ID, NEW_TINKER_ID configField(“key”, “value”), 預設我們自動從基準安裝包與新安裝包的Manifest中讀取tinkerId,並自動寫入configField。在這裡,你可以定義其他的資訊,在執行時可以通過TinkerLoadResult.getPackageConfigByName得到相應的數值。但是建議直接通過修改程式碼來實現,例如BuildConfig。 sevenZip
7zip路徑配置項,執行前提是useSign為true
引數 預設值 描述 zipArtifact null 例如”com.tencent.mm:SevenZip:1.1.10”,將自動根據機器屬性獲得對應的7za執行檔案,推薦使用。 path 7za 系統中的7za路徑,例如”/usr/local/bin/7za”。path設定會覆蓋zipArtifact,若都不設定,將直接使用7za去嘗試。
Extension
之前文章有介紹過,Extension中的屬性和gralde的的配置是一一對應的.上面的gradle的擴充套件block一共有7個, 那麼在外掛中也要創建出7個Extension物件來對映對應的屬性.
TinkerPatchExtension
Tinker全域性配置的自定義擴充套件, 對映Gradle中
tinkerPatch
的屬性配置, 並提供屬性校驗的共有方法, 主要效驗oldApk
屬性是否有效並且指向的檔案是否存在,否則丟擲Gradle異常.void checkParameter() { if (oldApk == null) { throw new GradleException("old apk is null, you must set the correct old apk value!") } File apk = new File(oldApk) if (!apk.exists()) { throw new GradleException("old apk ${oldApk} is not exist, you must set the correct old apk value!") } else if (!apk.isFile()) { throw new GradleException("old apk ${oldApk} is a directory, you must set the correct old apk value!") } }
TinkerBuildConfigExtension
編譯相關配置項的自定義擴充套件, 對映Gradle中
buildConfig
的屬性配置,並提供屬性校驗的共有方法, 主要效驗tinkerId
屬性是否有效,否則丟擲Gradle異常.void checkParameter() { if (tinkerId == null || tinkerId.isEmpty()) { throw new GradleException("you must set your tinkerId to identify the base apk!") } }
TinkerDexExtension
dex相關配置項的自定義擴充套件, 對映Gradle中
dex
的屬性配置,並提供校驗dexMode
屬性是否為raw | jar
方法,不在正常範圍內就丟擲Gradle異常.void checkDexMode() { if (!dexMode.equals("raw") && !dexMode.equals("jar")) { throw new GradleException("dexMode can be only one of 'jar' or 'raw'!") } }
TinkerLibExtension
lib相關配置項的自定義擴充套件,對映Gradle中
lib
支援更新的路徑集合.TinkerResourceExtension
資源相關配置項的自定義擴充套件,對映Gradle中
res
的屬性配置,並校驗largeModeSize
是否有效, 否則丟擲Gradle異常.void checkParameter() { if (largeModSize <= 0) { throw new GradleException("largeModSize must be larger than 0") } }
TinkerPackageConfigExtension
用於生成補丁包中的’package_meta.txt’檔案,對映Gradle
packageConfig
中的配置屬性, 並對外暴露訪問這些map屬性的方法.同時還提供了獲取基準包manifest中meta的方法,但是這些方法在這個版本中並沒有使用.TinkerSevenZipExtension
7zip路徑配置項, 對映Grade
sevenZip
中的屬性.獲取到以groupId:artifactId:version為格式拼裝的zipArtifact
,並在外掛執行的過程中建立起來對該artifact的依賴, 並最終獲取到配置依賴的執行檔案路徑.
Task
TinkerPatchSchemaTask
負責校驗Extensions的引數和環境是否合法和補丁生成.這個Task牽扯的東西太多了,後面單獨開一篇介紹.
TinkerManifestTask
建立Tinker的manifest任務,在manifestTask任務生成之後執行,並向android manifest檔案的application層級中插入Tinker_ID,供app執行時使用.過程是先校驗gradle中tinkerId是否設定.
String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId if (tinkerValue == null || tinkerValue.isEmpty()) { throw new GradleException('tinkerId is not set!!!') }
再利用
XmlParser
解析manifest檔案, 如果manifest檔案的application層級下已經有TINKER_ID了就先刪除掉.def metaDataTags = application['meta-data'] // remove any old TINKER_ID elements def tinkerId = metaDataTags.findAll { it.attributes()[ns.name].equals(TINKER_ID) }.each { it.parent().remove(it) }
並將gradle中配置的tinker_id插入到manifest中.
application.appendNode('meta-data', [(ns.name): TINKER_ID, (ns.value): tinkerValue]) // Write the manifest file def printer = new XmlNodePrinter(new PrintWriter(manifestPath, "utf-8")) printer.preserveWhitespace = true printer.print(xml)
最後拷貝修改過的manifest檔案到tinker的中間編譯路徑
build/intermediates/tinker_intermediates/
下.供開發者檢視.File manifestFile = new File(manifestPath) if (manifestFile.exists()) { FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML)) project.logger.error("tinker gen AndroidManifest.xml in ${MANIFEST_XML}") }
TinkerResourceIdTask
該任務獲取到
buildConfig.applyResourceMapping
配置的R檔案中的對映, 並將它keep到補丁包生成的過程中.這個Task會跟TinkerPatchSchemaTask一起展開講.TinkerProguardConfigTask
如果開啟了混淆,就會在gradle外掛中構建出該任務,主要的作用是將tinker中預設的混淆資訊和基準包的mapping資訊加入混淆列表,這樣就可以通過gradle配置自動幫開發者做一些類的混淆設定,並且可以通過applymapping的基準包的mapping檔案達到在混淆上補丁包和基準包一致的目的.首先開啟在編譯路徑下的混淆檔案,為後面寫入預設的keep規則做準備.檔案的路徑同樣在
tinker_intermediates
下.def file = project.file(PROGUARD_CONFIG_PATH) project.logger.error("try update tinker proguard file with ${file}") // Create the directory if it doesnt exist already file.getParentFile().mkdirs() // Write our recommended proguard settings to this file FileWriter fr = new FileWriter(file.path)
如果gradle中配置的基準包mapping檔案有效, 就將基準包的mapping檔案apply進來.
String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping //write applymapping if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) { project.logger.error("try add applymapping ${applyMappingFile} to build the package") fr.write("-applymapping " + applyMappingFile) fr.write("\n") }
如果使用插樁模式, 則需要keep插樁涉及到的類和方法.
if (project.tinkerPatch.dex.usePreGeneratedPatchDex) { def additionalKeptRules = "-keep class ${AuxiliaryClassInjector.NOT_EXISTS_CLASSNAME} { \n" + ' *; \n' + '}\n' + '\n' + '-keepclassmembers class * { \n' + ' <init>(...); \n' + ' static void <clinit>(...); \n' + '}\n' fr.write(additionalKeptRules) fr.write('\n') }
將dex.loader中配置的類在混淆的時候也keep起來.
Iterable<String> loader = project.extensions.tinkerPatch.dex.loader for (String pattern : loader) { if (pattern.endsWith("*") && !pattern.endsWith("**")) { pattern += "*" } fr.write("-keep class " + pattern) fr.write("\n") } fr.close()
最終將上面拼裝起來的混淆檔案新增進混淆檔案列表中使其生效.
applicationVariant.getBuildType().buildType.proguardFiles(file) def files = applicationVariant.getBuildType().buildType.getProguardFiles() project.logger.error("now proguard files is ${files}")
TinkerMultidexConfigTask
如果開啟了multiDex 會在編譯中根據gradle的配置和預設配置生成出要keep在main dex中的proguard資訊檔案,然後copy出這個檔案,方便開發者使用
multiDexKeepProguard
進行配置.首先開啟檔案並寫入預設配置.檔案路徑也在tinker_intermediates
下.def file = project.file(MULTIDEX_CONFIG_PATH) project.logger.error("try update tinker multidex keep proguard file with ${file}") // Create the directory if it doesn't exist already file.getParentFile().mkdirs() // Write our recommended proguard settings to this file FileWriter fr = new FileWriter(file.path) fr.write(MULTIDEX_CONFIG_SETTINGS) fr.write("\n")
將dex.loader中配置的class也keep進main dex.寫完檔案之後開發者就可以將整個檔案配置起來.
Iterable<String> loader = project.extensions.tinkerPatch.dex.loader for (String pattern : loader) { if (pattern.endsWith("*")) { if (!pattern.endsWith("**")) { pattern += "*" } } fr.write("-keep class " + pattern + " {\n" + " *;\n" + "}\n") fr.write("\n") } fr.close()
Plugin
上面講了用於接收和校驗gradle擴充套件塊屬性的Extension和用於處理各個不同任務的task.而Plugin物件既是整個Gradle外掛的入口又可以看成是Extension跟task的連結器.
構建Extension物件
最先做的就是構建出與gradle擴充套件相對應的7個Extension物件.
project.extensions.create('tinkerPatch', TinkerPatchExtension) project.tinkerPatch.extensions.create('buildConfig', TinkerBuildConfigExtension, project) project.tinkerPatch.extensions.create('dex', TinkerDexExtension, project) project.tinkerPatch.extensions.create('lib', TinkerLibExtension) project.tinkerPatch.extensions.create('res', TinkerResourceExtension) project.tinkerPatch.extensions.create('packageConfig', TinkerPackageConfigExtension, project) project.tinkerPatch.extensions.create('sevenZip', TinkerSevenZipExtension, project)
驗證和配置預設android gradle屬性
首先驗證外掛執行的gradle是不是application,不是的話直接crash掉.
if (!project.plugins.hasPlugin('com.android.application')) { throw new GradleException('generateTinkerApk: Android Application plugin required') }
再通過外掛project拿到android gradle的Extension.去除一些打包時不需要的檔案.
def android = project.extensions.android //add the tinker anno resource to the package exclude option android.packagingOptions.exclude("META-INF/services/javax.annotation.processing.Processor") android.packagingOptions.exclude("TinkerAnnoApplication.tmpl")
接著修改android的dexOptions屬性, 開啟jumboMode並關閉preDexLibraries選項.如果開啟preDexLibraries則可以脫離library編譯出dex,用來輔助incremental編譯. 開啟了可能會影響到tinker生成補丁.
def configuration = project.tinkerPatch //open jumboMode android.dexOptions.jumboMode = true //close preDexLibraries try { android.dexOptions.preDexLibraries = false } catch (Throwable e) { //no preDexLibraries field, just continue }
註冊插樁transform
由於Tinker在當前版本還支援回退qzone方案,所以肯定還是有插樁的動作,在gradle 1.5.0之前是根據preDex任務掌握時機使用asm或javasist做插樁,而gralde 1.5.0開始gradle就提供了Transform元件,可以用來做編譯期間處理中間資料.Tinker的插樁就是基於Transform和asm實現的.具體的實現這裡先不展開,後面會專門寫一篇關於Tinker插樁的文件.
android.registerTransform(new AuxiliaryInjectTransform(project))
打印出Tinker的修改宣告
通過gradle的logger打印出Tinker修改了哪些檔案或者屬性.
遍歷variant 根據不同的variant名字建立tasks
如果開啟了instant run直接crash掉
def instantRunTask = project.tasks.getByName("transformClassesWithInstantRunFor${variantName}") if (instantRunTask) { throw new GradleException( "Tinker does not support instant run mode, please trigger build" + " by assemble${variantName} or disable instant run" + " in 'File->Settings...'." ) }
根據當前variant構建出PatchSchemaTask任務, 用來初始化patch環境,驗證Extension引數和生成補丁.
TinkerPatchSchemaTask tinkerPatchBuildTask = project.tasks.create("tinkerPatch${variantName}", TinkerPatchSchemaTask) tinkerPatchBuildTask.dependsOn variant.assemble tinkerPatchBuildTask.signConfig = variant.apkVariantData.variantConfiguration.signingConfig variant.outputs.each { output -> tinkerPatchBuildTask.buildApkPath = output.outputFile File parentFile = output.outputFile tinkerPatchBuildTask.outputFolder = "${parentFile.getParentFile().getParentFile().getAbsolutePath()}/" + TypedValue.PATH_DEFAULT_OUTPUT + "/" + variant.dirName }
建立manifest任務,在manifestTask任務生成之後執行,並向android manifest檔案中插入TINKER_ID,供app執行時使用.
TinkerManifestTask manifestTask = project.tasks.create("tinkerProcess${variantName}Manifest", TinkerManifestTask) manifestTask.manifestPath = variantOutput.processManifest.manifestOutputFile manifestTask.mustRunAfter variantOutput.processManifest variantOutput.processResources.dependsOn manifestTask
如果開啟了混淆,就會在gradle外掛中構建出該任務,主要的作用是將tinker中預設的混淆資訊和基準包的mapping資訊加入混淆列表,這樣就可以通過gradle配置自動幫開發者做一些類的混淆設定,並且可以通過applymapping的基準包的mapping檔案達到在混淆上補丁包和基準包一致的目的.
boolean proguardEnable = variant.getBuildType().buildType.minifyEnabled if (proguardEnable) { TinkerProguardConfigTask proguardConfigTask = project.tasks.create("tinkerProcess${variantName}Proguard", TinkerProguardConfigTask) proguardConfigTask.applicationVariant = variant variantOutput.packageApplication.dependsOn proguardConfigTask }
如果開啟了multiDex 會在編譯中根據gradle的配置和預設配置生成出要keep在main dex中的proguard資訊檔案,然後copy這個檔案到
tinker_intermediates
下,方便開發者使用.boolean multiDexEnabled = variant.apkVariantData.variantConfiguration.isMultiDexEnabled() if (multiDexEnabled) { TinkerMultidexConfigTask multidexConfigTask = project.tasks.create("tinkerProcess${variantName}MultidexKeep", TinkerMultidexConfigTask) multidexConfigTask.applicationVariant = variant variantOutput.packageApplication.dependsOn multidexConfigTask }
這裡把Tinker的Gradle外掛流程梳理了一邊,牽扯到複雜功能流程的像補丁生成的task,R檔案處理task和插樁實現.這些後面會單獨分析.