Android Tinker used to achieve hot update

In fact, the market has a platform to help us integrate Tinker hot update, as well as providing patch management background, graphical interface operation, etc., such as bugly platform, TinkerPatch platform TinkerPatchSDK integration , as long as they provide the integrated SDK can be integrated relatively More convenient. This article does not explain the way this integrated platform, because the platform has a detailed official documentation. This article is mainly based on official documents given Tinker, an access hot update, because Tinker official document to the complex. You can refer to Tinker access to official guidelines . And paper uses Gradle access mode.

First, add a dependency in the Project / build.gradle in

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

Second, adding a dependency on app / build.gradle in

 // 多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')

Third, the configuration information tinker

Add app / build.gradle closure document 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\""

Add the closure android

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

Define a directory for storing a local storage path after the package apk, R.txt, mapping.txt file

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

Defines constants used to store information reference apk package (apk, R.txt, mapping.txt), each time to patch packages, we need to replace the path here, so while each release of a new package, to apk new package, R.txt, mapping.txt save a copy of these three files, so need to use the time to patch packages.

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

The following are some methods for obtaining specific values

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
}

Which getTinkerIdValue () This method is used to obtain tinkerid, as long as we ensure that every time the patch package, this is not the same id can be used directly versionCode value. In this way each release a new package, this value will change.

Attached below a complete build.gradle configuration files, those at the bottom of Tinker configuration is usually no need to modify

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

                }
            }
        }
    }
}

Fourth, create Application

Tinker recommended wording: Write a class that inherits DefaultApplicationLike, their application logic is written on the inside onCreate () method. The following sample code official, pay attention here @DefaultLifeCycle inside SampleApplication take names at random, but in application and androidManifest file name consistent.

Fifth, registered a load patch callback result of Service

Operation is made in service after you successfully loaded hot update plug-in, you will be prompted to update successfully, and here to do a lock-screen operation will load hot update plug-ins. Service class specific code directly to the official copy of the line. SampleResultService class file , there is a utility class Utils tools , do not forget the list of registered services Yo.

Sixth, in the code can call to load patches

In order to facilitate our side, directly to the patch file in the root directory of the SD card test, the actual project, this patch should be issued by the server.

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

This, you have completed the access of Tinker, the next step is to test.

What we need is practical effect, without the need to install the patch from the new package is loaded in, click on the button to load the patch, a patch loading, after loading successfully, the next time you enter the app, bug will automatically be repaired.

1, playing standard package (a bug in the package)

Open the Android Studio of Gradle interface, double-click assembleDebug or assembleRelease

After this operation, it will generate and R apk file in the build directory of bakApk, if there is confusion turned on, there will be mapping file

The apk installed to the phone

Second, the patch package

First, copy the generated above apk, R, mapping file names to lower the ext gradle app, replacing the name before, and Sync Now look.

Modify the bug, the bug after completion of modification, call patch

Or in gradle interface, double-click tinker under tinkerPatchDebug or tinkerPatchRelease

In the build directory, patch_signed_7zip.apk is what we need to patch, copy it to the root directory of phone sdcard

 In the code path and click the button to load the patch above sd card root path consistent. The actual project should be patch to the background, and then downloaded to the phone's sd card or other paths.

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

The final step, load the patch. Click to load the patch, if successful service will be prompted to "patch success, please restart process" (if not prompted successful, check the steps and path if there are problems), kill the process or turn off the screen, enter the App again, you will find out the patch has been loaded a, sd card patches will automatically cleaned up.

These are integrated into the entire process of the project according to the official documents given Tinker. Of course, the actual project can be directly integrated TinkerPatchSDK, the patch to TinkerPatch platform to manage, we do not need to worry about loading them.

 

 

Published 23 original articles · won praise 19 · views 2130

Guess you like

Origin blog.csdn.net/huyinda/article/details/105194702