Tinker1.9.9 gradle接入指南

前言

Tinker是什麼

Tinker是微信官方的Android熱補丁解決方案,它支持動態下發代碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實現更新。當然,你也可以使用Tinker來更新你的插件。

它主要包括以下幾個部分:

  1. gradle編譯插件: tinker-patch-gradle-plugin
  2. 核心sdk庫: tinker-android-lib
  3. 非gradle編譯用戶的命令行版本: tinker-patch-cli.jar

爲什麼使用Tinker

當前市面的熱補丁方案有很多,其中比較出名的有阿里的AndFix、美團的Robust以及QZone的超級補丁方案。但它們都存在無法解決的問題,這也是正是我們推出Tinker的原因。

  Tinker QZone AndFix Robust
類替換 yes yes no no
So替換 yes no no no
資源替換 yes yes no no
全平臺支持 yes yes yes yes
即時生效 no no yes yes
性能損耗 較小 較大 較小 較小
補丁包大小 較小 較大 一般 一般
開發透明 yes yes no no
複雜度 較低 較低 複雜 複雜
gradle支持 yes no no no
Rom體積 較大 較小 較小 較小
成功率 較高 較高 一般 最高

總的來說:

  1. AndFix作爲native解決方案,首先面臨的是穩定性與兼容性問題,更重要的是它無法實現類替換,它是需要大量額外的開發成本的;
  2. Robust兼容性與成功率較高,但是它與AndFix一樣,無法新增變量與類只能用做的bugFix方案;
  3. Qzone方案可以做到發佈產品功能,但是它主要問題是插樁帶來Dalvik的性能問題,以及爲了解決Art下內存地址問題而導致補丁包急速增大的。

特別是在Android N之後,由於混合編譯的inline策略修改,對於市面上的各種方案都不太容易解決。而Tinker熱補丁方案不僅支持類、So以及資源的替換,它還是2.X-8.X(1.9.0以上支持8.X)的全平臺支持。利用Tinker我們不僅可以用做bugfix,甚至可以替代功能的發佈。Tinker已運行在微信的數億Android設備上,那麼爲什麼你不使用Tinker呢?

1.添加gradle依賴

1.1 配置項目build.gradle

在項目的build.gradle添加tinker-patch-gradle-plugin的依賴:

classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"

1.2 tinker相關配置

在項目目錄下新建tinkerconfig.gradle文件,用來存放tinker相關的配置:

apply plugin: 'com.tencent.tinker.patch'

def gitSha() {
    return "XXXXXXXXXXXX"
}

def bakPath = file("${buildDir}/bakApk/")

/**
 * you can use assembleRelease to build you base apk
 * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
 * add apk from the build/bakApk
 */
ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-release-1229-16-14-42.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-release-1229-16-14-42-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-release-1229-16-14-42-R.txt"

    //only use for build all flavor, if not, just ignore this field
//    tinkerBuildFlavorDirectory = "${bakPath}/app-release-1229-14-15-29"
}


def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}

def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'

    tinkerPatch {
        oldApk = getOldApkPath()
        ignoreWarning = false
        useSign = true
        tinkerEnable = buildWithTinker()

        buildConfig {
            applyMapping = getApplyMappingPath()
            applyResourceMapping = getApplyResourceMappingPath()

            tinkerId = getTinkerIdValue()
            keepDexApply = false
            isProtectedApp = false
            supportHotplugComponent = true
        }

        dex {
            dexMode = "jar"
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             * necessary,default '[]'
             * Warning, it is very very important, loader classes can't change with patch.
             * thus, they will be removed from patch dexes.
             * you must put the following class into main dex.
             * Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
             * own tinkerLoader, and the classes you use in them
             *
             */
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            pattern = ["lib/*/*.so"]
        }

        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * optional,default '[]'
             * the resource file exclude patterns, ignore add, delete or modify resource change
             * it support * or ? pattern.
             * Warning, we can only use for files no relative with resources.arsc
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             * default 100kb
             * for modify resource, if it is larger than 'largeModSize'
             * we would like to use bsdiff algorithm to reduce patch file size
             */
            largeModSize = 100
        }

        packageConfig {
            configField("patchMessage", "tinker is sample to use")
            configField("platform", "all")
            /**
             * patch version via packageConfig
             */
            configField("patchVersion", "1.0")
        }
        //or you can add config filed outside, or get meta value from old apk
        //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
        //project.tinkerPatch.packageConfig.configField("test2", "sample")

        /**
         * if you don't use zipArtifact or path, we just use 7za to try
         */
        sevenZip {
            /**
             * optional,default '7za'
             * the 7zip artifact path, it will use the right 7za with your platform
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * optional,default '7za'
             * you can specify the 7za path yourself, it will overwrite the zipArtifact value
             */
            path = "D:\\software\\SevenZip-1.1.10-windows-x86_64.exe"
        }
    }

    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    def date = new Date().format("MMdd-HH-mm-ss")

    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name

        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                        from variant.outputs.first().outputFile
                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    project.afterEvaluate {
        //sample use for build all flavor for one time
        if (hasFlavors) {
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"

                    }

                }
            }

            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
                    }

                }
            }
        }
    }
}



task sortPublicTxt() {
    doLast {
        File originalFile = project.file("public.txt")
        File sortedFile = project.file("public_sort.txt")
        List<String> sortedLines = new ArrayList<>()
        originalFile.eachLine {
            sortedLines.add(it)
        }
        Collections.sort(sortedLines)
        sortedFile.delete()
        sortedLines.each {
            sortedFile.append("${it}\n")
        }
    }
}

以上是參考tinker官方demo進行修改過的。

修改的地方有:

1) gitSha方法返回值的修改:改成你對應的tinkerId,可以去tinker平臺生成一個。

鏈接:http://www.tinkerpatch.com/Apps/detail/id/9721

2)ext{}中存放的是跟打差分包相關的參數,只有在需要打差分包的時候才需要修改:

// 打開tinker開關 
tinkerEnabled = true :
//基線版本的apk包的名稱
tinkerOldApkPath = "${bakPath}/app-release-1229-16-38-39.apk"
//生成基線版本的apk包的時候一起生成的mapping文件
tinkerApplyMappingPath = "${bakPath}/app-release-1229-16-38-39-mapping.txt"
//生成基線版本的apk包的時候一起生成的R文件
tinkerApplyResourcePath = "${bakPath}/app-release-1229-16-38-39-R.txt"

以上三個文件目錄在使用命令:assembleRelease後,都會在本地生成:

每次調用assembleRelease或assembleDebug時,都會在bakApk目錄下生成一個新的apk文件,時間可以進行區分。

注意:因爲clean的時候清除掉基線APK,所以每次打基本版本的時候,一定記得備份這三個文件!

3)修改:supportHotplugComponent = true

下面是它給的註釋:

/**
  * optional, default 'false'
  * Whether tinker should support component hotplug (add new component dynamically).
  * If this attribute is true, the component added in new apk will be available after
  * patch is successfully loaded. Otherwise an error would be announced when generating patch
  * on compile-time.
  *
  * <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
  */

翻譯:修補程序是否應該支持組件熱插拔(動態添加新組件)。如果該屬性爲真,則添加到新apk中的組件將在之後可用補丁加載成功。否則,在生成補丁時將宣佈錯誤在編譯時。

 

4)修改:path = "D:\\software\\SevenZip-1.1.10-windows-x86_64.exe"

看來下注釋,這行代碼會優先:zipArtifact = "com.tencent.mm:SevenZip:1.1.10"配置。

因爲在我的電腦報錯了:

大概是找不到這個工具,而且還給了個鏈接,然後點擊鏈接進行下載配置好路徑就可以了!沒有的話可以留言郵箱。

1.3 app/build.gradle配置

首先導入我們第二步中新建的文件:

1)apply from: '../tinkerconfig.gradle'

2)添加android參數配置,來個完整的,

def javaVersion = JavaVersion.VERSION_1_7

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.victor.tinkerdemo"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        multiDexEnabled true
        buildConfigField "String", "MESSAGE", "\"I am the base apk\""
//        buildConfigField "String", "MESSAGE", "\"I am the patch apk\""
        /**
         * client version would update with patch
         * so we can get the newly git version easily!
         */
        buildConfigField "String", "TINKER_ID", "\"9d1a1432426d7316\""
        buildConfigField "String", "PLATFORM", "\"all\""
    }
    compileOptions {
        sourceCompatibility javaVersion
        targetCompatibility javaVersion
    }
    //recommend
    dexOptions {
        jumboMode = true
    }
    signingConfigs {
        release {
            try {
                storeFile file("./keystore/release.keystore")
                storePassword "testres"
                keyAlias "testres"
                keyPassword "testres"
            } catch (ex) {

            }
        }

        debug {
            storeFile file("./keystore/debug.keystore")
        }
    }
    buildTypes {
        release {
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

主要就是配置配置一下tinkerId的參數和打版本時的參數,都好理解。

3)導入tinker依賴包

implementation("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }

2.編譯和產生patch包

我們將用到兩組任務:

如果是debug版本,則用assembleDebug和tinkerPatchDebug;如果是release版本則用assembleRelease和tinkerPatchRelease。

當我們使用assembleDebug或assembleRelease命令生成了apk後,會在本地bakApk目錄下生成對應的三個文件:

然後將以下三個參數按照名稱進行修改:

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-release-1229-16-38-39.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-release-1229-16-38-39-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-release-1229-16-38-39-R.txt"

    //only use for build all flavor, if not, just ignore this field
//    tinkerBuildFlavorDirectory = "${bakPath}/app-release-1229-14-15-29"
}

最後在使用tinkerPatchDebug或assembleRelease命令生成patch包。

最後的patch_signed_7zip.apk就是我們需要的差分包了。

3.使差分包生效

3.1 差分包下發

1)可以使用tinker平臺的方式來下發管理

2)從後臺獲取

不管怎樣,都是下載到SD卡或手機,從本地進行加載。

3.2 patch生效

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");

這個方法主要就是把加載路徑告訴它即可。

注意:調用完加載方法後,需要重啓APP才能生效。

另外在demo中我們還看到它還可以加載library。

但是我有發現在部分手機上需要點擊多次load才能加載成功。

 

參考鏈接:

https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97

https://github.com/Tencent/tinker/wiki

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章