1. 程式人生 > >Android 熱修復方案Tinker(六) Gradle外掛實現

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的分配,這樣不僅可以減少補丁包的大小,同時也避免由於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’檔案,對映GradlepackageConfig中的配置屬性, 並對外暴露訪問這些map屬性的方法.同時還提供了獲取基準包manifest中meta的方法,但是這些方法在這個版本中並沒有使用.

  • TinkerSevenZipExtension

    7zip路徑配置項, 對映GradesevenZip中的屬性.獲取到以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

    1. 如果開啟了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...'."
          )
      }
    2. 根據當前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
      }
    3. 建立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
    4. 如果開啟了混淆,就會在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
      }
    5. 如果開啟了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和插樁實現.這些後面會單獨分析.