Android中使用Tinker实现热更新

其实现在市场上已经有平台帮我们集成了Tinker热更新,还有提供补丁包管理后台,图形界面操作等等,比如bugly平台,TinkerPatch 平台TinkerPatchSDK集成,只要集成他们提供的SDK就可以,集成也相对比较方便。本文就不讲解这种平台的集成方式了,因为平台上都有详细的官方文档说明。本篇文章主要基于Tinker官方给出的文档,进行热更新的接入,因为Tinker官方文档给的比较复杂。可参照Tinker 官方接入指南。且本文主要采用Gradle方式接入。

一、在Project/build.gradle中添加依赖

dependencies {
   classpath 'com.android.tools.build:gradle:3.5.3'
   classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')
}

二、在app/build.gradle中添加依赖

 // 多dex 打包的类库
 implementation "androidx.multidex:multidex:2.0.1"
 //可选,用于生成application类
 provided('com.tencent.tinker:tinker-android-anno:1.9.1')
 //tinker的核心库
 implementation('com.tencent.tinker:tinker-android-lib:1.9.1')

三、配置tinker相关信息

在app/build.gradle文件defaultConfig闭包中添加

  //由于报annotation错误才引入这一句,可不用
  javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
  // tinker 基本配置
  multiDexEnabled true
  buildConfigField "String", "MESSAGE", "\"I am the base apk\""
  buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
  buildConfigField "String", "PLATFORM", "\"all\""

在android闭包添加

// Tinker 推荐设置
    dexOptions {
        jumboMode = true
    }
    configurations.all {
        resolutionStrategy.force 'com.android.support:support-annotations:27.1.1'
    }

定义一个目录,用来存放打包后本地apk,R.txt,mapping.txt文件的存放路径

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

定义一些常量,用来保存基准apk包的相关信息(apk,R.txt,mapping.txt),每次要打补丁包时,都需要替换这里的路径,所以在每一次发布新包的同时,要将新包的apk,R.txt,mapping.txt这三个文件保存一份,以便打补丁包时需要用到。

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
    //每次要打补丁包的时候,都需要替换这边的基准apk路径
    tinkerOldApkPath = "${bakPath}/tinkerfixapp-release-0330-17-30-49.apk"
    //proguard mapping file to build patch apk
    //每次要打补丁包的时候,都需要替换这边的基准apk中的mapping文件路径
    tinkerApplyMappingPath = "${bakPath}/tinkerfixapp-release-0330-17-30-49-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    //每次要打补丁包的时候,都需要替换这边的基准apk中的R文件路径
    tinkerApplyResourcePath = "${bakPath}/tinkerfixapp-release-0330-17-30-49-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-0421-17-11-26"
}

以下是提供一些方法,用来获取具体的值

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 100
}
def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

其中getTinkerIdValue()这个方法用来获取tinkerid,我们只要保证每一次打补丁包时,这个id不一样即可,可以直接使用versionCode的值。这样每一次发布一个新包时,这个值都会变化。

下面附上一个完整的build.gradle文件的配置信息,底部的那些是Tinker的配置,一般无需修改

apply plugin: 'com.android.application'
android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.example.tinkerfixapp"
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        //由于报annotation错误才引入这一句,可不用
        javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
        // tinker 基本配置
        multiDexEnabled true
        buildConfigField "String", "MESSAGE", "\"I am the base apk\""
        buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
        buildConfigField "String", "PLATFORM", "\"all\""
    }

    buildTypes {
        release {
            minifyEnabled false
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            minifyEnabled false
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    // Tinker 推荐设置
    dexOptions {
        jumboMode = true
    }
    configurations.all {
        resolutionStrategy.force 'com.android.support:support-annotations:27.1.1'
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // 多dex 打包的类库
    implementation "androidx.multidex:multidex:2.0.1"
    //可选,用于生成application类
    provided('com.tencent.tinker:tinker-android-anno:1.9.1')
    //tinker的核心库
    implementation('com.tencent.tinker:tinker-android-lib:1.9.1')
}

def bakPath = file("${buildDir}/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}/tinkerfixapp-release-0330-17-30-49.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/tinkerfixapp-release-0330-17-30-49-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/tinkerfixapp-release-0330-17-30-49-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-0421-17-11-26"
}

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

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

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}
//以下内容一般无需修改,直接拷贝即可

if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'

    tinkerPatch {
        /**
         * 默认为null
         * 将旧的apk和新的apk建立关联
         * 从build / bakApk添加apk
         */
        oldApk = getOldApkPath()
        /**
         * 可选,默认'false'
         *有些情况下我们可能会收到一些警告
         *如果ignoreWarning为true,我们只是断言补丁过程
         * case 1:minSdkVersion低于14,但是你使用dexMode与raw。
         * case 2:在AndroidManifest.xml中新添加Android组件,
         * case 3:装载器类在dex.loader {}不保留在主要的dex,
         * 它必须让tinker不工作。
         * case 4:在dex.loader {}中的loader类改变,
         * 加载器类是加载补丁dex。改变它们是没有用的。
         * 它不会崩溃,但这些更改不会影响。你可以忽略它
         * case 5:resources.arsc已经改变,但是我们不使用applyResourceMapping来构建
         */
        ignoreWarning = false

        /**
         *可选,默认为“true”
         * 是否签名补丁文件
         * 如果没有,你必须自己做。否则在补丁加载过程中无法检查成功
         * 我们将使用sign配置与您的构建类型
         */
        useSign = true

        /**
         可选,默认为“true”
         是否使用tinker构建
         */
        tinkerEnable = buildWithTinker()

        /**
         * 警告,applyMapping会影响正常的android build!
         */
        buildConfig {
            /**
             *可选,默认为'null'
             * 如果我们使用tinkerPatch构建补丁apk,你最好应用旧的
             * apk映射文件如果minifyEnabled是启用!
             * 警告:你必须小心,它会影响正常的组装构建!
             */
            applyMapping = getApplyMappingPath()
            /**
             *可选,默认为'null'
             * 很高兴保持资源ID从R.txt文件,以减少java更改
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             *必需,默认'null'
             * 因为我们不想检查基地apk与md5在运行时(它是慢)
             * tinkerId用于在试图应用补丁时标识唯一的基本apk。
             * 我们可以使用git rev,svn rev或者简单的versionCode。
             * 我们将在您的清单中自动生成tinkerId
             */
            tinkerId = getTinkerIdValue()

            /**
             *如果keepDexApply为true,则表示dex指向旧apk的类。
             * 打开这可以减少dex diff文件大小。
             */
            keepDexApply = false
        }

        dex {
            /**
             *可选,默认'jar'
             * 只能是'raw'或'jar'。对于原始,我们将保持其原始格式
             * 对于jar,我们将使用zip格式重新包装dexes。
             * 如果你想支持下面14,你必须使用jar
             * 或者你想保存rom或检查更快,你也可以使用原始模式
             */
            dexMode = "jar"

            /**
             *必需,默认'[]'
             * apk中的dexes应该处理tinkerPatch
             * 它支持*或?模式。
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             *必需,默认'[]'
             * 警告,这是非常非常重要的,加载类不能随补丁改变。
             * 因此,它们将从补丁程序中删除。
             * 你必须把下面的类放到主要的dex。
             * 简单地说,你应该添加自己的应用程序{@code tinker.sample.android.SampleApplication}
             * 自己的tinkerLoader,和你使用的类
             *
             */
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
                    "tinker.sample.android.app.BaseBuildInfo"
            ]
        }

        lib {
            /**
             可选,默认'[]'
             apk中的图书馆应该处理tinkerPatch
             它支持*或?模式。
             对于资源库,我们只是在补丁目录中恢复它们
             你可以得到他们在TinkerLoadResult与Tinker
             */
            pattern = ["lib/armeabi/*.so"]
        }

        res {
            /**
             *可选,默认'[]'
             * apk中的什么资源应该处理tinkerPatch
             * 它支持*或?模式。
             * 你必须包括你在这里的所有资源,
             * 否则,他们不会重新包装在新的apk资源。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             *可选,默认'[]'
             *资源文件排除模式,忽略添加,删除或修改资源更改
             * *它支持*或?模式。
             * *警告,我们只能使用文件没有relative与resources.arsc
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             *默认100kb
             * *对于修改资源,如果它大于'largeModSize'
             * *我们想使用bsdiff算法来减少补丁文件的大小
             */
            largeModSize = 100
        }

        packageConfig {
            /**
             *可选,默认'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE'
             * 包元文件gen。路径是修补程序文件中的assets / package_meta.txt
             * 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()
             * 或TinkerLoadResult.getPackageConfigByName
             * 我们将从旧的apk清单为您自动获取TINKER_ID,
             * 其他配置文件(如下面的patchMessage)不是必需的
             */
            configField("patchMessage", "tinker is sample to use")
            /**
             *只是一个例子,你可以使用如sdkVersion,品牌,渠道...
             * 你可以在SamplePatchListener中解析它。
             * 然后你可以使用补丁条件!
             */
            configField("platform", "all")
            /**
             * 补丁版本通过packageConfig
             */
            configField("patchVersion", "1.0")
        }
        //或者您可以添加外部的配置文件,或从旧apk获取元值
        //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
        //project.tinkerPatch.packageConfig.configField("test2", "sample")

        /**
         * 如果你不使用zipArtifact或者path,我们只是使用7za来试试
         */
        sevenZip {
            /**
             * 可选,默认'7za'
             * 7zip工件路径,它将使用正确的7za与您的平台
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * 可选,默认'7za'
             * 你可以自己指定7za路径,它将覆盖zipArtifact值
             */
//        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[0].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

Tinker的推荐写法:写一个类继承DefaultApplicationLike,自己的application的逻辑则写在onCreate()方法里面。下面的官方示例代码,注意这里@DefaultLifeCycle里面的SampleApplication的名字随意取,但是和androidManifest文件中application中name保持一致。

五、注册一个加载补丁回调结果的Service

service中所做的操作是在你加载成功热更新插件后,会提示你更新成功,并且这里做了锁屏操作就会加载热更新插件。具体Service类的代码直接拷贝官方给的就行。SampleResultService类文件,还有一个工具类Utils工具类,别忘了在清单中注册服务呦。

六、在代码中可以调用加载补丁包

我们这边为了方便,直接将补丁包文件放在了SD卡的根目录下测试,实际项目中这个补丁包应该是由服务器下发的。

    /**
     * 加载热补丁插件
     */
    public void loadPatch() {
        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                Environment.getExternalStorageDirectory().getAbsolutePath() + 
          "/patch_signed_7zip.apk");
    }

到此,你已经完成了Tinker的接入了,接下来就是进行测试。

我们需要的实际效果是,在不需要从新打包安装的情况下将补丁加载进来,点击加载补丁按钮,进行补丁的加载,加载成功后,下次进入app时,bug就会自动被修复了。

1、打基准包(有bug的包)

打开Android Studio的Gradle 界面,双击 assembleDebug 或者 assembleRelease

该操作之后,就会在build的bakApk目录下生成apk和R文件,如果有开启混淆,也会有mapping文件

将该apk安装到手机上

二、打补丁包

首先将上面生成的apk,R,mapping的文件名复制到app的gradle中ext下,替换之前的名称,并Sync Now一下。

修改bug,将bug修改完之后,再打补丁包

还是在gradle界面,双击tinker下tinkerPatchDebug 或者tinkerPatchRelease

在build文件目录下,patch_signed_7zip.apk就是我们所需要的补丁包,将其拷贝到手机的sdcard的根目录中

 在代码中点击加载补丁按钮路径和上面sd卡根路径保持一致。实际项目中补丁包应该是给后台,然后通过下载到手机的sd卡或其他路径下的。

/**
     * 加载热补丁插件
     */
    public void loadPatch() {
        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
    }

最后一步,加载补丁。点击加载补丁,如果成功service会提示"patch success, please restart process"(如果没提示成功,请检查步骤和路径是否有问题),杀死进程或关闭屏幕,再次进入App,会发现补丁已经加载出来了,sd卡中的补丁包也会自动清理掉。

以上就是根据Tinker官方给出的文档集成到项目的整个流程。当然实际项目中是可以直接集成TinkerPatchSDK,将补丁包交给TinkerPatch平台去管理,我们无需操心其中的加载过程。

发布了23 篇原创文章 · 获赞 19 · 访问量 2130

猜你喜欢

转载自blog.csdn.net/huyinda/article/details/105194702