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的原因。

 TinkerQZoneAndFixRobust
类替换yesyesnono
So替换yesnonono
资源替换yesyesnono
全平台支持yesyesyesyes
即时生效nonoyesyes
性能损耗较小较大较小较小
补丁包大小较小较大一般一般
开发透明yesyesnono
复杂度较低较低复杂复杂
gradle支持yesnonono
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<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,也就是我们的基线版本的唯一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后,都会在本地生成:

每次调用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)SevenZip报错

如果SevenZip报错,修改: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',必须放在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包

我们将用到两组任务:

如果是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。

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值