热修复Tinker的使用 + TinkerPatch补丁管理平台 gradle接入

来源:互联网 发布:电脑版淘宝微淘哪里看 编辑:程序博客网 时间:2024/05/18 12:44

开场白:我想快速集成并使用热修复Tinker,快速熟悉打补丁,及使用补丁管理平台

(本片文章并没有对知识点进行讲述,简单描述应用到项目中遇到的问题)

第一次写文章,来的各位爷,您就凑合凑合看吧。有描述的不对的地方,请留下您宝贵的建议。


步骤:

①集成Tinker,

②集成TinkerPatch

③打补丁

④使用管理平台进行发布补丁

开发环境

在开始集成前,我先说下我的开发环境:

项目build.gradle

dependencies {        classpath 'com.android.tools.build:gradle:2.2.2'        // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files    }

项目\gradle\wrapper\gradle-wrapper.properties文件中

distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip

app\build.gradle文件中

compileSdkVersion 25buildToolsVersion "25.0.3" defaultConfig {        ...        minSdkVersion 15        targetSdkVersion 25        ...}

1.集成Tinker,并使用Tinker打补丁

集成Tinker的时候,刚开始看官方文档有点懵逼,看了BigSweetee作者的一篇博客tinker热修复gradle接入才完成了集成,怪我学识太浅,Tinker接入指南确实有点难啃。BigSweetee作者的讲解对我刚使用热修复的人来说很是受用,我就不在这里赘(zhuì)述了。(最优的选择官方文档和源码)


2.集成TinkerPatch

若按照官方文档或上面的作者进行gradle接入,需要修改一些地方。上代码↓

准备工作:

①在gradle.properties文件中创建

TINKER_VERSION=1.8.1TINKERPATCH_VERSION=1.1.8

②在项目的build文件中

    dependencies {        classpath 'com.android.tools.build:gradle:2.2.2'//        classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"        //无需再单独引用tinker的其他库        classpath ("com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:${TINKERPATCH_VERSION}") { changing = true }        // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files    }

③配置app下的build

官方推荐build配置https://github.com/TinkerPatch/tinkerpatch-sample/blob/master/app/build.gradle

我并根据官方文档及BigSweetee作者中的build,进行优化和提出

下面并贴上我的整个build文件


apply plugin: 'com.android.application'dependencies {    compile fileTree(dir: 'libs', include: ['*.jar'])    compile 'com.android.support:appcompat-v7:25.3.1'    compile 'com.android.support.constraint:constraint-layout:1.0.2'    testCompile 'junit:junit:4.12'    //若使用annotation需要单独引用,对于tinker的其他库都无需再引用    provided("com.tinkerpatch.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }    compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:${TINKERPATCH_VERSION}") { changing = true }}android {    compileSdkVersion 25    buildToolsVersion "25.0.3"    //配置自己的签名文件    signingConfigs {        release {            try {                keyAlias 'release'                keyPassword '123456'                storeFile file('D:/PinkCandy/AndroidStudioProjects/MyShack/release.jks')                storePassword '123456'            } catch (ex) {                throw new InvalidUserDataException(ex.toString())            }        }        debug {            storeFile file('D:/PinkCandy/AndroidStudioProjects/MyShack/debug.jks')            keyAlias 'debug'            keyPassword '123456'            storePassword '123456'        }    }    defaultConfig {        applicationId "com.yjh.mytinkerpatch"        minSdkVersion 19        targetSdkVersion 25        versionCode 1        versionName "1.0"        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"        multiDexEnabled true        multiDexKeepProguard file("tinkerMultidexKeep.pro") //keep specific classes using proguard syntax    }    buildTypes {        release {            minifyEnabled true            shrinkResources true            signingConfig signingConfigs.release            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }        debug {            debuggable true            minifyEnabled false            signingConfig signingConfigs.debug            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }    sourceSets {        main {            jniLibs.srcDirs = ['libs']        }    }}def bakPath = file("${buildDir}/bakApk/")def variantName = "1101-16-44-30"def buildType = "release"/** * 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    //for normal build    //old apk file to build patch apk    tinkerOldApkPath = "${bakPath}/mytinkerpatch-${buildType}-${variantName}.apk"    //proguard mapping file to build patch apk    tinkerApplyMappingPath = "${bakPath}/mytinkerpatch-${buildType}-${variantName}-mapping.txt"    //resource R.txt to build patch apk, must input if there is resource changed    tinkerApplyResourcePath = "${bakPath}/${buildType}-${variantName}-R.txt"    //only use for build all flavor, if not, just ignore this field    tinkerBuildFlavorDirectory = "${bakPath}/mytinkerpatch-1101-13-57-40"}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 buildWithTinker() {    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled}def getTinkerBuildFlavorDirectory() {    return ext.tinkerBuildFlavorDirectory}if (buildWithTinker()) {    apply plugin: 'com.tencent.tinker.patch'    tinkerPatch {        /**         * necessary,default 'null'         * the old apk path, use to diff with the new apk to build         * add apk from the build/bakApk         */        oldApk = getOldApkPath()        /**         * optional,default 'false'         * there are some cases we may get some warnings         * if ignoreWarning is true, we would just assert the patch process         * case 1: minSdkVersion is below 14, but you are using dexMode with raw.         *         it must be crash when load.         * case 2: newly added Android Component in AndroidManifest.xml,         *         it must be crash when load.         * case 3: loader classes in dex.loader{} are not keep in the main dex,         *         it must be let tinker not work.         * case 4: loader classes in dex.loader{} changes,         *         loader classes is ues to load patch dex. it is useless to change them.         *         it won't crash, but these changes can't effect. you may ignore it         * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build         */        ignoreWarning = true        /**         * optional,default 'true'         * whether sign the patch file         * if not, you must do yourself. otherwise it can't check success during the patch loading         * we will use the sign config with your build type         */        useSign = true        /**         * optional,default 'true'         * whether use tinker to build         */        tinkerEnable = buildWithTinker()        /**         * Warning, applyMapping will affect the normal android build!         */        buildConfig {            /**             * optional,default 'null'             * if we use tinkerPatch to build the patch apk, you'd better to apply the old             * apk mapping file if minifyEnabled is enable!             * Warning:             * you must be careful that it will affect the normal assemble build!             */            applyMapping = getApplyMappingPath()            /**             * optional,default 'null'             * It is nice to keep the resource id from R.txt file to reduce java changes             */            applyResourceMapping = getApplyResourceMappingPath()            /**             * necessary,default 'null'             * because we don't want to check the base apk with md5 in the runtime(it is slow)             * tinkerId is use to identify the unique base apk when the patch is tried to apply.             * we can use git rev, svn rev or simply versionCode.             * we will gen the tinkerId in your manifest automatic             */            tinkerId = "1.0"/*getTinkerIdValue()*/            /**             * if keepDexApply is true, class in which dex refer to the old apk.             * open this can reduce the dex diff file size.             */            keepDexApply = false        }        dex {            /**             * optional,default 'jar'             * only can be 'raw' or 'jar'. for raw, we would keep its original format             * for jar, we would repack dexes with zip format.             * if you want to support below 14, you must use jar             * or you want to save rom or check quicker, you can use raw mode also             */            dexMode = "jar"            /**             * necessary,default '[]'             * what dexes in apk are expected to deal with tinkerPatch             * it support * or ? pattern.             */            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 {            /**             * optional,default '[]'             * what library in apk are expected to deal with tinkerPatch             * it support * or ? pattern.             * for library in assets, we would just recover them in the patch directory             * you can get them in TinkerLoadResult with Tinker             */            pattern = ["lib/*/*.so"]        }        res {            /**             * optional,default '[]'             * what resource in apk are expected to deal with tinkerPatch             * it support * or ? pattern.             * you must include all your resources in apk here,             * otherwise, they won't repack in the new apk resources.             */            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 {            /**             * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'             * package meta file gen. path is assets/package_meta.txt in patch file             * you can use securityCheck.getPackageProperties() in your ownPackageCheck method             * or TinkerLoadResult.getPackageConfigByName             * we will get the TINKER_ID from the old apk manifest for you automatic,             * other config files (such as patchMessage below)is not necessary             */            configField("patchMessage", "tinker is sample to use")            /**             * just a sample case, you can use such as sdkVersion, brand, channel...             * you can parse it in the SamplePatchListener.             * Then you can use patch conditional!             */            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 = "/usr/local/bin/7za"        }    }    List<String> flavors = new ArrayList<>();    project.android.productFlavors.each { flavor ->        flavors.add(flavor.name)    }    boolean hasFlavors = flavors.size() > 0    /**     * bak apk and mapping     */    android.applicationVariants.all { variant ->        /**         * task type, you want to bak         */        def taskName = variant.name        def date = new Date().format("MMdd-HH-mm-ss")        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.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"                    }                }            }        }    }}apply from: 'tinkerpatch.gradle'


==========================不太华丽的分割线build此时已经贴完===========================

在build最后还有一个tinkerpatch.gradle,此gradle是连接管理补丁后台作用,拉取补丁并整合再到应用,这个过程我没去研究。

官方文档的tinkerpatch.gradle:https://github.com/TinkerPatch/tinkerpatch-sample/blob/master/app/tinkerpatch.gradle

下面贴上我的tinkerpatch.gradle文件


apply plugin: 'tinkerpatch-support'/** * TODO: 请按自己的需求修改为适应自己工程的参数 */def bakPath = file("${buildDir}/bakApk/")def variantName = "1101-16-44-30"def buildType = "release"/** * 对于插件各参数的详细解析请参考 * http://tinkerpatch.com/Docs/SDK */tinkerpatchSupport {    /** 可以在debug的时候关闭 tinkerPatch **/    /** 当disable tinker的时候需要添加multiDexKeepProguard和proguardFiles,     这些配置文件本身由tinkerPatch的插件自动添加,当你disable后需要手动添加     你可以copy本示例中的proguardRules.pro和tinkerMultidexKeep.pro,     需要你手动修改'tinker.sample.android.app'本示例的包名为你自己的包名, com.xxx前缀的包名不用修改     **/    tinkerEnable = true    reflectApplication = false    autoBackupApkPath = "${bakPath}"    appKey = "c5e9524735f29f95"    /** 注意: 若发布新的全量包, appVersion一定要更新 **/    appVersion = "1.0.0"    //for normal build    //old apk file to build patch apk    baseApkFile = "${bakPath}/mytinkerpatch-${buildType}-${variantName}.apk"    //proguard mapping file to build patch apk    baseProguardMappingFile = "${bakPath}/mytinkerpatch-${buildType}-${variantName}-mapping.txt"    //resource R.txt to build patch apk, must input if there is resource changed    baseResourceRFile = "${bakPath}/mytinkerpatch-${buildType}-${variantName}-R.txt"    /**     *  若有编译多flavors需求, 可以参照: https://github.com/TinkerPatch/tinkerpatch-flavors-sample     *  注意: 除非你不同的flavor代码是不一样的,不然建议采用zip comment或者文件方式生成渠道信息(相关工具:walle 或者 packer-ng)     **/    /** 若有编译多flavors需求,可在flavors中覆盖以下参数     *  你也可以直接通过tinkerPatchAllFlavorDebug/tinkerPatchAllFlavorRelease, 一次编译所有的flavor补丁包     *  注意的是:除非你不同的flavor代码是不一样的,不然建议采用zip comment或者文件方式生成渠道信息     **/    productFlavors {        flavor {            flavorName = "yingyongbao"            appVersion = "${tinkerpatchSupport.appVersion}_${flavorName}"            baseApkFile = "${bakPath}/mytinkerpatch-${buildType}-${variantName}.apk"            baseProguardMappingFile = "${bakPath}/mytinkerpatch-${buildType}-${variantName}-mapping.txt"            baseResourceRFile = "${bakPath}/mytinkerpatch-${buildType}-${variantName}-R.txt"            /** 开启加固开关,上传此flavor的apk到加固网站进行加固 **/            protectedApp = true        }        flavor {            flavorName = "360shoujizhushou"            appVersion = "${tinkerpatchSupport.appVersion}_${flavorName}"            baseApkFile = "${bakPath}/mytinkerpatch-${buildType}-${variantName}.apk"            baseProguardMappingFile = "${bakPath}/mytinkerpatch-${buildType}-${variantName}-mapping.txt"            baseResourceRFile = "${bakPath}/mytinkerpatch-${buildType}-${variantName}-R.txt"            /** 开启加固开关,上传此flavor的apk到加固网站进行加固 **/            protectedApp = true        }    }}/** * 用于用户在代码中判断tinkerPatch是否被使能 */android {    defaultConfig {        buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}"    }}/** * 一般来说,我们无需对下面的参数做任何的修改 * 对于各参数的详细介绍请参考: * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 */tinkerPatch {    ignoreWarning = false    useSign = true    dex {        dexMode = "jar"        pattern = ["classes*.dex"]        loader = []    }    lib {        pattern = ["lib/*/*.so"]    }    res {        pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]        ignoreChange = []        largeModSize = 100    }    packageConfig {    }    sevenZip {        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"//        path = "/usr/local/bin/7za"    }    buildConfig {        keepDexApply = false    }}


此时还差两个文件,我是用官方文档推荐的

proguardRules.pro和tinkerMultidexKeep.pro

此时已经接入成功并配置成功!!!

3.使用

初始化Application

@SuppressWarnings("unused")@DefaultLifeCycle(application = ".SampleApplication",        flags = ShareConstants.TINKER_ENABLE_ALL,        loadVerifyFlag = false)public class SampleApplicationLike extends DefaultApplicationLike {    private static final String TAG = "tinkerPatch";    public SampleApplicationLike(Application application,                                 int tinkerFlags,                                 boolean tinkerLoadVerifyFlag,                                 long applicationStartElapsedTime,                                 long applicationStartMillisTime,                                 Intent tinkerResultIntent) {        super(application,                tinkerFlags,                tinkerLoadVerifyFlag,                applicationStartElapsedTime,                applicationStartMillisTime,                tinkerResultIntent);    }    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)    @Override    public void onBaseContextAttached(Context base) {        super.onBaseContextAttached(base);        //you must install multiDex whatever tinker is installed!        MultiDex.install(base);    }    @Override    public void onCreate() {        super.onCreate();        initTinker();    }// 初始化TinkerPatch SDK    private void initTinker() {        if (BuildConfig.TINKER_ENABLE) {            //开始检查是否有补丁,这里配置的是每隔访问3小时服务器是否有更新。            TinkerPatch.init(this)                    .reflectPatchLibrary()                    .setPatchRollbackOnScreenOff(true)                    .setPatchRestartOnSrceenOff(true)                    .setFetchPatchIntervalByHours(3);            // 获取当前的补丁版本            Log.d(TAG, "current patch version is " + TinkerPatch.with().getPatchVersion());            //每隔3个小时去访问后台时候有更新,通过handler实现轮训的效果            TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();        }    }}

在清单文件下添加name属性,刚才初始化Application的时候并没有以真正的以来Application,而是以来了DefaultApplicationLike类,此时的name填上在这句话的application = “”名字

@DefaultLifeCycle(application = ".SampleApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)


<application        android:name=".SampleApplication"        android:icon="@mipmap/ic_launcher"        android:label="@string/app_name"        android:theme="@style/AppTheme">        <activity android:name=".MainActivity">            <intent-filter>                <action android:name="android.intent.action.MAIN"/>                <category android:name="android.intent.category.LAUNCHER"/>            </intent-filter>        </activity>    </application>

现在如果你直接运行程序,会报错,application未初始化,此时只需要rebuild一下就好了,清单文件里的name就不会显示红色了

此时全部的配置已经进行完毕,剩下的就是进行编译程序,打包,打补丁,进行使用管理平台了


在build工具里打开app下的build工具中assembleRelease,进行打包



此时会在app\build\bakApk目录下生成文件



在你的项目中更改一些代码逻辑,或者控件的属性:如TextView的textColor属性等,更改后,进行开始打补丁



随后生成如下目录


这个文件就是我们需要的最终的补丁文件


把此补丁发布到管理平台上进行升级就ok了



MainActivity和布局https://github.com/TinkerPatch/tinkerpatch-easy-sample/blob/master/app/src/main/java/com/tinkerpatch/easy_sample/MainActivity.java
附上官方文档的TinkerPatch平台使用说明地址:http://www.tinkerpatch.com/Docs/start

注意:
①不混淆,mapping文件就没有
②旧版本名称要与gradle文件中的名称一致,不然报找不到XXXX-XX 文件
③Application类并没有直接集成,需要rebuild一下,自动创建Application类
保存好旧版本的三个文件

⑤多看官方文档。


希望您读完后,能够给些宝贵意见

原创粉丝点击