tinker热修护—gradle接入

由于原理与系统限制,Tinker有以下已知问题
  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
  2. 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
  3. 在Android N上,补丁对应用启动时间有轻微的影响;
  4. 不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”;
  5. 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
接入Tinker的几种方式

TinkerPatch 平台
提供了补丁后台托管,版本管理,保证传输安全等功能,让你无需搭建一个后台,无需关心部署操作,只需引入一个 SDK 即可立即使用 Tinker。

第三方平台
如TinkerPatch平台 和Bugly热更新功能
这种方式对Application进行了反射,是有风险
反射失败的情况,我们会自动回退到代理 Application
生命周期模式,防止因为反射失败而造成应用无法启动的问题。

自己后台管理patch包
主要介绍这种
1. 命令行接入
2. gradle接入


gradle接入

介绍:通过gradle脚本生成patch包
然后让 old.apk(对应版本打的修护包)加载patch包

这里写图片描述

这里写图片描述

第一步,引入Tinker插件和依赖

工程根目录下的build.gradle的dependencies中

buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.2')
    }
}

主module中的build.gradle文件里

// tinker config
apply from: rootProject.file('tinker_support.gradle') 

android {
    //依赖配置
    dependencies {
        provided('com.tencent.tinker:tinker-android-anno::1.9.2')
        //tinker的核心库
        compile('com.tencent.tinker:tinker-android-lib::1.9.2')
    }
}

创建tinker_support.gradle 在最外层的build.gradle

tinker_support.gradle

//基准apk包的备份路径,这里仅作备份用,
//每次构建apk时会将生成的apk文件、mapping和R文件自动拷贝一份到这个目录下去
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?
    // /** 可以在debug的时候关闭 tinkerPatch **/
    tinkerEnabled = true

    //构建时需要在此配置基准包的filename
    tinkerBaseApkFileName = getTinkerBaseApkFileName("upwallet-debug-0118-10-11-12.apk")
    //proguard mapping file to build patch apk
    tinkerMappingFileName = tinkerBaseApkFileName.substring(0, tinkerBaseApkFileName.length() - 4) + "-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerSymbolFileName = tinkerBaseApkFileName.substring(0, tinkerBaseApkFileName.length() - 4) + "-R.txt"

    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/upwallet-debug-0118-10-11-12.apk"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/"+tinkerSymbolFileName

    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/"+tinkerMappingFileName

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

//构建补丁时获取基准apk包的文件名
def getTinkerBaseApkFileName(def defaultName) {
    return hasProperty("TINKER_BASE_APK_NAME") ? TINKER_BASE_APK_NAME : defaultName
}

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

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
}


/**
 * Tinker插件配置信息
 */
if (buildWithTinker()) {
    // 依赖tinker插件
    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' 可选,默认false
         * 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。
         * 因为这些情况可能会导致编译出来的patch包带来风险:
         * case 1: minSdkVersion小于14,但是dexMode的值为"raw";
         * case 2: 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
         * case 3: 定义在dex.loader用于加载补丁的类不在main dex中;
         * case 4:  定义在dex.loader用于加载补丁的类出现修改;
         * case 5: resources.arsc改变,但没有使用applyResourceMapping编译。
         */
        ignoreWarning = false

        /**
         * optional,default 'true'
         * 可选,默认true, 验证基准apk和patch签名是否一致
         * 运行过程中需要验证基准apk包与补丁包的签名是否一致,是否需要签名。
         * 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()

        // 用于生成补丁包中的'package_meta.txt'文件
        buildConfig {
            tinkerId = BUILD_TOOLS_VERSION // 必选,
            /**
             * if keepDexApply is true, class in which dex refer to the old apk.
             * open this can reduce the dex diff file size.
             * 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。
             * 若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
             */
            keepDexApply = false

            /**
             * optional,default 'null'
             * 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,
             * 这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * optional,default 'null'
             * 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,
             * 从而减少补丁包的大小。这个只是推荐设置,
             * 不设置applyMapping也不会影响任何的assemble编译。
             */
            applyMapping = getApplyMappingPath()

            /**
             * optional, default 'false'
             * 是否使用加固模式,仅仅将变更的类合成补丁。
             * 注意,这种模式仅仅可以用于加固应用中。
             */
            isProtectedApp = false


            /**
             * 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>
             */
            supportHotplugComponent = false
        }
        dex {
            /**
             * 只能是'raw'或者'jar'。
             * 对于'raw'模式,我们将会保持输入dex的格式。
             * 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。
             * 如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。
             * 默认我们并不会去校验md5,一般情况下选择jar模式即可。
             */
            dexMode = "jar"

            /**
             * necessary,default '[]'
             * 需要处理dex路径,支持*、?通配符,必须使用'/'分割。
             * 路径是相对安装包的,例如assets/...
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             * necessary,default '[]'
             * 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。
             * 这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
             * 这里需要定义的类有:
             * 1. 你自己定义的Application类;
             * 2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
             * 3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
             * 4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。
             *    这里需要注意的是,这些类的直接引用类也需要加入到loader中。
             *    或者你需要将这个类变成非preverify。
             * 5. 使用1.7.6版本之后版本,参数1、2会自动填写。
             *
             */
            loader = [
                    //Tinker库中用于加载补丁包的部分类
                    "tinker.sample.android.app.BaseBuildInfo",
            ]
        }

        /**
         * lib相关的配置项
         */
        lib {
            /**
             * optional,default '[]'
             * 需要处理lib路径,支持*、?通配符,必须使用'/'分割。
             * 与dex.pattern一致, 路径是相对安装包的,例如assets/...
             */
            pattern = ["lib/*/*.so"]
        }
        /**
         * res相关的配置项
         */
        res {
            /**
             * optional,default '[]'
             * 需要处理res路径,支持*、?通配符,必须使用'/'分割。
             * 与dex.pattern一致, 路径是相对安装包的,例如assets/...,
             * 务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * optional,default '[]'
             * 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。
             * 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             * default 100kb
             * 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。
             * 这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
             */
            largeModSize = 100
        }

        /**
         * 用于生成补丁包中的'package_meta.txt'文件
         */
        packageConfig {
            /**
             * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
             * configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。
             * 在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
             * 但是建议直接通过修改代码来实现,例如BuildConfig。
             */
            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补丁版本
             */
            configField("patchVersion", "1.0")
        }
        /**
         *  7zip路径配置项,执行前提是useSign为true
         */
        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
    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"
                    }
                }
            }
        }
    }

}
代码application AndroidManifest.xml

改造自己的Application,详细可参考自定义Application类

我们在AndroidManifest.xml会自己定义一个application文件 如图:

这里写图片描述

替换成如图(名字可以自己取,位置最好和以前application同级):
虽然我们这么写了,但是实际上Application会在编译期生成
意思就是不需要去创建一个类,build它会自己生成
如果报红,也可以build下

这里写图片描述

AndroidManifest还需要添加

<meta-data
    android:name="TINKER_ID"
    android:value="最好是自己的版本号" />

<!--tinker 根据情况是否需要-->
<service
    android:name="com.tinker.service.SampleResultService"
    android:exported="false"/>

以前Application修改为

//tinker推荐下面的写法
//UPTinkerApplication 是application 
@DefaultLifeCycle(application = "com.xxxxx.base.UPTinkerApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class UPApplication extends ApplicationLike { 

   public UPApplication(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    } 

    /**
     * install multiDex before install tinker
     * so we don't need to put the tinker lib classes in the main dex
     *
     * @param base
     */
    @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);

        TinkerManager.setTinkerApplicationLike(this);

        TinkerManager.initFastCrashProtect();
        //should set before tinker is installed
        TinkerManager.setUpgradeRetryEnable(true);

        //optional set logIml, or you can use default debug log
        TinkerInstaller.setLogIml(new MyLogImp());

        //installTinker after load multiDex
        //or you can put com.tencent.tinker.** to main dex
        TinkerManager.installTinker(this);
        Tinker tinker = Tinker.with(getApplication());
    }
}

onBaseContextAttached 配置是根据 Tinker提供的Demo编入
当然也可以不按照Demo写

Tinker Demo

引入文件如下 不做分析 UPTinkerUtils自己写的工具类

这里写图片描述

腾讯提供的Demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值