在HotFix SDK中接入Tinker


title: HotFix&&Tinker融合调研文档 tags: hotfix,tinker,热修复 grammar_cjkRuby: true

两年前的调研,准备离职了不想白费之前的汗水因此发出来php

在HotFix SDK Library中接入Tinker

1. 指定Tinker SDK版本

gradle.propertites中指定tinker接入版本,例如:java

TINKER_VERSION=1.7.7
复制代码

2. 添加Tinker gradle依赖

  • 项目build.gradle中添加tinker-patch-gradle-plugin的依赖
dependencies {
        classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
    }
复制代码
  • HotFix SDK library模块下的build.gradle中添加tinker的库依赖
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
复制代码

3. 在HotFix SDK 代理Application中初始化Tinker

  • 将HotFixProxyApplication 继承自TinkerApplication 继承继承自TinkerApplication须要复写其构造方法,方法实现直接调用父类的方法。
super(tinker_flag);
复制代码

这里能够考虑多写一个代理Application直接继承Application(即原HotFixProxyApplication),若是不打算接入Tinker,则能够直接在app模块的AndroidManifest.xml使用该代理Applicationandroid

  • 在runOriginalApplication方法中的反射调用完原Application的attach方法后进行Tinker的初始化工做
private void installTinker(Application application) {
        if (isInstalled) {
            TinkerLog.w(TAG, "install tinker, but has installed, ignore");
            return;
        }
        Intent tinkerResultIntent = new Intent();
        try {
            //reflect tinker loader, because loaderClass may be define by user!
            Class<?> tinkerLoadClass = Class.forName(TinkerLoader.class.getName(), false, getClassLoader());

            Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class, int.class, boolean.class);
            Constructor<?> constructor = tinkerLoadClass.getConstructor();
            tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this, TINKER_FLAG, tinkerLoadVerifyFlag);
        } catch (Throwable e) {
            //has exception, put exception error code
            ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
            tinkerResultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
        }
        Tinker tinker = new Tinker.Builder(application).build();
        Tinker.create(tinker);
        tinker.install(tinkerResultIntent);
        isInstalled=true;
    }
复制代码

至此,在HotFix SDK中已经完成了Tinker SDK的最初步的接入了。git

4. 向服务器发起查询补丁请求,若是返回的补丁是Tinker补丁类型,下载完成校验经过则用Tinker SDK进行补丁安装。

目前查询补丁的请求参数能够不变,返回参数中加一个补丁是不是TInker补丁说明字段便可。github

当补丁下载成功后咱们就能够调用如下方法尝试安装Tinker补丁了api

TinkerInstaller.onReceiveUpgradePatch(Context context, String patchLocation)
复制代码

Tinker补丁生效实际上有3个过程,一是补丁检验,二是补丁合成,三是补丁加载。补丁加载过程是在应用重启后进行的。安全

  • 补丁检验 Tinker自带了补丁前置检验功能,固然是最基础的校验,由于在补丁的合成和加载的时候还会进行其余一些校验,补丁前置检验包括是否开启了Tinker补丁功能,补丁文件是否合法,当前进程是不是Tinker补丁服务进程等等。而咱们HotFix的XCHK校验就须要经过拓展Tinker补丁检验回调方法来实现,方法自定义一个PatchListener继承Tinker的DefaultPatchListenerpatchCheck方法,在其中进行XCHK校验方法。 此外,还能够在patchCheck方法中加入是不是谷歌Play渠道或者360渠道判断(所以谷歌渠道不容许代码下发,360渠道必需要通过360加固可是Tinker1.7.6之后不支持加固),应用得到的最大内存空间是否知足指定的最小内存空间要求等等。
  • 补丁合成 补丁合成结果会回调DefaultPatchReporter中的方法,而自定义PatchReporter继承DefaultPatchReporter,复写其中的回调方法。具体回调方法见下表
回调方法 描述
onPatchResult 这个是不管补丁合成失败或者成功都会回调的接口,它返回了本次合成的类型,时间以及结果等。默认咱们只是简单的输出这个信息,你能够在这里加上监控上报逻辑。
onPatchServiceStart 这个是Patch进程启动时的回调,咱们能够在这里进行一个统计的工做。
onPatchPackageCheckFail 补丁合成过程对输入补丁包的检查失败,这里能够经过错误码区分,例如签名校验失败、tinkerId不一致等缘由。默认咱们会删除临时文件。
onPatchVersionCheckFail 对patch.info的校验版本合法性校验。若校验失败,默认咱们会删除临时文件。
onPatchTypeExtractFail 从补丁包与原始安装包中合成某种类型的文件出现错误,默认咱们会删除临时文件。
onPatchDexOptFail 对合成的dex文件提早进行dexopt时出现异常,默认咱们会删除临时文件。
onPatchInfoCorrupted patch.info是用来管理补丁包版本的文件,这是在更新info文件时发生损坏的回调。默认咱们会卸载补丁包,由于此时咱们已经没法恢复了。
onPatchException 在补丁合成过程捕捉到异常,十分但愿你能够把错误信息反馈给咱们。默认咱们会删除临时文件,而且将tinkerFlag设为不可用。

若是补丁合成失败,则能够经过回调方法上报HotFix服务器的report接口。现有的HotFix补丁错误码能够讨论继续增长。服务器

须要注意的是,PatchReporter中有个onPatchResult方法,这个方法是在补丁合成进程中进行的补丁合成结果回调方法,还有一个TinkerResultService中也有一个onPatchResult方法,TinkerResultService是补丁合成进程将合成结果返回给主进程的服务,Tinker默认的DefaultTinkerResultService是会杀掉:patch进程,假设当前是补丁升级而且成功了,Tinker会杀掉当前进程,让补丁包更快的生效,如果修复类型的补丁包而且失败了,Tinker会卸载补丁包。app

Tinker默认的ResultService不是很符合咱们当前HotFix的业务逻辑,所以ResultService必须重写。能够参考例子的HotFixTinkerResultService,在当前应用在退入后台或手机锁屏时这两个时机杀掉当前进程去应用补丁。当前也能够提供接口让接入者选择重启时机。ide

  • 补丁加载 LoadReporter类定义了Tinker在加载补丁时的一些回调,Tinker为咱们提供了默认实现DefaultLoadReporter类,不过这确定不知足咱们的需求,由于在这里咱们须要上报补丁加载的结果。;例子中的TinkerLoadReporter是继承DefaultLoadReporter的类,在onLoadResult中能够进行补丁安装结果的上报,以及补丁加载失败也能够在这个方法中选择重试安装补丁。 补丁加载也有回调方法,见下表:
回调方法 描述
onLoadResult 这个是不管加载失败或者成功都会回调的接口,它返回了本次加载所用的时间、返回码等信息。默认咱们只是简单的输出这个信息,你能够在这里加上监控上报逻辑。
onLoadPatchListenerReceiveFail 全部的补丁合成请求都须要先经过PatchListener的检查过滤。此次检查不经过的回调,它运行在发起请求的进程。默认咱们只是打印日志
onLoadPatchVersionChanged 补丁包版本升级的回调,只会在主进程调用。默认咱们会杀掉其余全部的进程(保证全部进程代码的一致性),而且删掉旧版本的补丁文件。
onLoadFileNotFound 在加载过程当中,发现部分文件丢失的回调。默认如果dex,dex优化文件或者lib文件丢失,咱们将尝试从补丁包去修复这些丢失的文件。若补丁包或者版本文件丢失,将卸载补丁包。
onLoadFileMd5Mismatch 部分文件的md5与meta中定义的不一致。默认咱们为了安全考虑,依然会清空补丁。
onLoadPatchInfoCorrupted patch.info是用来管理补丁包版本的文件,这是info文件损坏的回调。默认咱们会卸载补丁包,由于此时咱们已经没法恢复了。
onLoadPackageCheckFail 加载过程补丁包的检查失败,这里能够经过错误码区分,例如签名校验失败、tinkerId不一致等缘由。默认咱们将会卸载补丁包
onLoadException 在加载过程捕捉到异常。默认咱们会直接卸载补丁包

根据HotFix业务需求,须要重写LoadReporteronLoadPatchVersionChanged方法,默认的onLoadPatchVersionChanged方法会杀掉其余全部的进程(保证全部进程代码的一致性),而且删掉旧版本的补丁文件。这就致使了HotFix监控进程也被杀掉,所以须要在这里排除掉HotFix监控进程,使其不被杀死。

Tinker补丁包生成

在须要集成HotFix SDK 的工程的build.gradle中加入Tinker补丁包生成gradle任务 这一部分能够参考Tinker官方接入指南示例,打包参数含义也有详尽的解释。

须要注意的是:在dex节点下的loader节点中加入须要排除的类,包括自定义的Application和HotFix SDK的类。

下面是我的的使用例子:

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

    //for normal build
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-release-0228-15-14-21.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-release-0228-15-14-21-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-release-0228-15-14-21-R.txt"

    //only use for build all flavor, if not, just ignore this field
//    tinkerBuildFlavorDirectory = "${bakPath}/app-0217-16-54-37"
}

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() {
    //versionCode做为TinkerId,这样就不须要git和commit一次
    return android.defaultConfig.versionCode + ""
}

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

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'
    tinkerPatch {
        oldApk = getOldApkPath()
        ignoreWarning = true
        useSign = true
        tinkerEnable = buildWithTinker();

        buildConfig {
            applyMapping = getApplyMappingPath()
            applyResourceMapping = getApplyResourceMappingPath()
            tinkerId = getTinkerIdValue()
            keepDexApply = false
        }

        dex {
            dexMode = "jar"
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "com.tencent.tinker.loader.*",
                    "com.cn21.HotTinker.MyApp",
                    "com.cn21.hotfix.*"
            ]
        }

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

        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            ignoreChange = ["assets/sample_meta.txt"]
            largeModSize = 100
        }

        packageConfig {
            configField("patchMessage", "tinker is sample to use")
            configField("platform", "all")
            configField("patchVersion", "1.0")
        }

        sevenZip {
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"

        }
    }

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

                }
            }
        }
    }
} else {
//HotFix插件配置
    apply plugin: "cn.jiajixin.nuwa"
    nuwa {
        isCloseHotfixPatch = false
        oldBuildHotfixDir = "${project.getProjectDir()}/old"
        gradleBuildVersion = "1.5.0"
        excludeClass = ["com.cn21.HotTinker.MyApp",
                        "android/support/multidex/MultiDex.class",
                        "com/cn21/hotfix/HotFixManager.class"]
        excludePackage = ["com/tencent/tinker/lib",
                          "com/tencent/tinker/loader",
                          "com/tencent/tinker/commons"]
    }
}
复制代码

示例工程HotTinker改动说明

示例工程HotTinker是在HotFix SDK工程的基础上作出如下改动:

  • 修改HotFixProxyApplication
    • 使其继承TinkerApplication,添加构造函数并直接调用super(TINKER_FLAG);
    • 添加 private void installTinker(Application application) 方法,并在runOriginalApplication中调用,进行Tinker的初始化
    • 这里考虑多写一个直接继承Application的HotFix应用代理,当接入者不考虑接入Tinker时能够避免没必要要的消耗。
  • 新增com.cn21.hotfix.tinkerReporter包,在这个包下新增了:
    • HotFixTinkerReport类:在这个类中定义了真正的上报接口Reporter不过并无真正实现,而TinkerLoadReporter和TinkerPatchReporter都是调用了HotFixTinkerReport中上报方法,具体的上报须要Reporter实现类去完成。这里须要完成的是根据HotFix的API协议修改HotFixTinkerReport类中上报结果码,以及实现Reporter接口完成真正的打补丁结果上报HotFix服务器。
      • TinkerLoadReporter类:Tinker补丁加载结果回调类,这里须要补充的是补丁加载回调结果上报以及重写onLoadPatchVersionChanged方法,onLoadPatchVersionChanged方法默认会杀掉主进程外的其余全部的进程(保证全部进程代码的一致性),不过Tinker补丁基本不会对HotFix SDK的监控服务进程进行修改,所以须要在这个方法中排除掉HotFix SDK的监控服务进程。
      • TinkerPatchReporter类:Tinker补丁合成结果回调类,这里须要补充的是若是合成失败须要进行上报失败缘由给HotFix服务器,若是合成失败能够选择重试(可选)。可参考Tinker官方示例工程tinker-sample-androidUpgradePatchRetry
      • TinkerPatchListener类:Tinker补丁前置检查,在这里进行了应用是否为谷歌play渠道检查,内存空间大小检查以及补丁文件是否存在检查等等,须要补充的是HotFix补丁xchk检查以及是不是360渠道检查(不支持360渠道)
  • utils包下新增TinkerUtils类
  • service包下新增HotFixTinkerResultService类:补丁合成进程将合成结果返回给主进程的类,须要修改onPatchResult方法,由于默认的实现是补丁合成成功后就当即杀死当前应用进程,而这种方式确定不行的,HotFixTinkerResultService的作法是在用户锁屏的时候重启应用,固然也能够在其余合适的时机重启应用,还可让接入者在Manifest进行配置选择重启时机。
  • 将app模块中的AndroidManifest.xmlHotFixService移到了library模块的AndroidManifest.xml,并添加了自定义的HotFixTinkerResultService service节点。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.cn21.hotfix">

    <application>
        <!-- HotFix 服务-->
        <service android:name="com.cn21.hotfix.service.HotFixService" android:process=":hotfix"/>
        <service android:name="com.cn21.hotfix.service.HotFixTinkerResultService" android:exported="false"/>
    </application>
</manifest>
复制代码

附:HotFix与Tinker兼容示例工程:HotTinker

相关文章
相关标签/搜索