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 "1.0.0"
}

def bakPath = file("${buildDir}/bakApk/")
def bakPatchName = "App-ali-release-0319-15-36-44"
/**
 * 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}/${bakPatchName}.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/${bakPatchName}-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/${bakPatchName}-R.txt"

    //only use for build all flavor, if not, just ignore this field
//    tinkerBuildFlavorDirectory = "${bakPath}/App-ali-release-0315-18-06-53"
}


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")
            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:\\soft\\7z1900-x64.exe"
        }
    }

    List 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 sortedLines = new ArrayList<>()
        originalFile.eachLine {
            sortedLines.add(it)
        }
        Collections.sort(sortedLines)
        sortedFile.delete()
        sortedLines.each {
            sortedFile.append("${it}\n")
        }
    }
}

以上是参考tinker官方demo进行修改过的。

修改的地方有:

1) gitSha方法返回值的修改:改成你对应的tinkerId,也就是我们的基线版本的唯一id,一般我们用版本号来确定唯一性,如V1.0.0等等。

 

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"

这是demo中的内容,为了方便,我将三个文件的名称统一设置了一下:

def bakPath = file("${buildDir}/bakApk/")
def bakPatchName = "App-ali-release-0319-15-36-44"
/**
 * 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}/${bakPatchName}.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/${bakPatchName}-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/${bakPatchName}-R.txt"

    //only use for build all flavor, if not, just ignore this field
//    tinkerBuildFlavorDirectory = "${bakPath}/App-ali-release-0315-18-06-53"
}

以上三个文件目录在使用命令:assembleRelease后,都会在本地生成:

Tinker1.9.9 gradle接入指南_第1张图片

每次调用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.
  *
  * Notice that currently this feature is incubating and only support NON-EXPORTED Activity
  */

翻译:修补程序是否应该支持组件热插拔(动态添加新组件)。如果该属性为真,则添加到新apk中的组件将在之后可用补丁加载成功。否则,在生成补丁时将宣布错误在编译时。

 

4)SevenZip报错

如果SevenZip报错,修改:path = "D:\\software\\SevenZip-1.1.10-windows-x86_64.exe"

看来下注释,这行代码会优先:zipArtifact = "com.tencent.mm:SevenZip:1.1.10"配置。

因为在我的电脑报错了:

Tinker1.9.9 gradle接入指南_第2张图片

大概是找不到这个工具,而且还给了个链接,然后点击链接进行下载配置好路径就可以了!没有的话可以留言邮箱。

1.3 app/build.gradle配置

首先导入我们第二步中新建的文件:

1)apply from: '../tinkerconfig.gradle',必须放在app主工程中,不然报错。

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包

我们将用到两组任务:

Tinker1.9.9 gradle接入指南_第3张图片

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

当我们使用assembleDebug或assembleRelease命令生成了apk后,会在本地bakApk目录下生成对应的三个文件:

Tinker1.9.9 gradle接入指南_第4张图片

然后将以下三个参数按照名称进行修改:

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包。

Tinker1.9.9 gradle接入指南_第5张图片

最后的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。

4.注意事项

4.1 各个module使用的各种库的版本不一致,将导致打包失败,有时候提示还不好找

4.2 只支持到java7,跟要求使用java8的库冲突。其中我遇到的就是butterknife9.0.0要求使用java8,所以结果就是不使用butterknife了。

但是,后来在使用时,新建一个tinker模块,把tinker模块配置使用:

def javaVersion = JavaVersion.VERSION_1_7

并没有影响其他模块使用lamda表达式。

4.3 如果打开了混淆配置,请注意混淆配置

4.控制差分包生效时机

SampleResultService类负责监听热更新是否成功,tinker官方demo是一旦合并成功,就直接杀死进程退出。
if (result.isSuccess) {
    TinkerLoadResult tinkerLoadResult = Tinker.with(this).getTinkerLoadResultIfPresent();

    Log.e(TAG, "合并成功, current version : " + tinkerLoadResult.currentVersion + ", result version : " + result.patchVersion);
    deleteRawPatchFile(new File(result.rawPatchFilePath));

    //not like TinkerResultService, I want to restart just when I am at background!
    //if you have not install tinker this moment, you can use TinkerApplicationHelper api
    if (checkIfNeedKill(result)) {
        if (Utils.isBackground()) {
            TinkerLog.i(TAG, "it is in background, just restart process");
            restartProcess();
        } else {
            //we can wait process at background, such as onAppBackground
            //or we can restart when the screen off
            TinkerLog.i(TAG, "tinker wait screen to restart process");
            new Utils.ScreenState(getApplicationContext(), new Utils.ScreenState.IOnScreenOff() {
                @Override
                public void onScreenOff() {
                    TinkerLog.i(TAG, "screen off, start kill app");
                    restartProcess();
                }
            });
        }
    } else {
        TinkerLog.i(TAG, "I have already install the newly patch version!");
    }
}

参考链接:

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

你可能感兴趣的:(安卓开发,框架)