小试牛刀 --- Tinker集成教程

项目中我们总会遇到这样的问题刚刚发布版本就发现了一个严重错误,对用户的使用体验非常的差,所以需要立马更新. 但是如果全量更新的话,小则就是20M APK大小, 多则 50多 M. 这样频繁的让用户下载非常影响用户体验。 事实上我所在的项目组一直都是这么干的,个人感觉这样非常low。

Tinker的作用

Tinker就是为了解决这种问题而生的, 修改少量的代码,生成差分包,然后用户下载非常小的更新包,就可以解决问题。它是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。
https://github.com/Tencent/tinker
下面这个是 官方的demo代码, 下载源码 单独跑这一个demo就可以测试tinker
https://github.com/Tencent/tinker/tree/master/tinker-sample-android

如何接入

废话不多说 直接贴上我的demo代码。

引入依赖

E:\xxx\TinkerDemo\build.gradle

    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.1'

        classpath ("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")
    }

E:\xxx\TinkerDemo\app\build.gradle

apply plugin: 'com.android.application'
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'

apply from: 'tinkerpatch.gradle' //引用tinkerpatch.gradle文件

android {
    compileSdkVersion project.COMPILE_BUILD_SDK_VERSION as int
    defaultConfig {
        applicationId "com.example.tinkerdemo"
        minSdkVersion project.MIN_SDK_VERSION as int
        targetSdkVersion project.TARGET_SDK_VERSION as int

        multiDexEnabled true

        versionCode project.VERSION_CODE as int
        versionName project.VERSION_NAME
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    signingConfigs {
        config {
            keyAlias 'key0'
            keyPassword '123456'
            storeFile file('../keystore.jks')
            storePassword '123456'
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.config  //gradlew assembleRelease
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    //tinker核心sdk库     参与编译与打包
    api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }

    annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }

    api 'com.android.support:multidex:1.0.3'
    api 'pub.devrel:easypermissions:2.0.1'
}

配置一些Tinker的相关参数
E:\xxx\TinkerDemo\app\tinkerpatch.gradle


def bakPath = file("${buildDir}/bakApk/") //指定基准文件存放位置
ext {
    tinkerEnable = true
    tinkerOldApkPath = "${bakPath}/app-release-0718-15-46-33.apk"
    tinkerID = "1.0"
    //tinkerApplyMappingPath = "${bakPath}/"
    tinkerApplyResourcePath = "${bakPath}/app-release-0718-15-46-33-R.txt"
}

def buildWithTinker() {
    return ext.tinkerEnable
}

def getOldApkPath() {
    return ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return ext.tinkerID
}

def getTinkerBuildFlavorDirectory(){
    return ext.tinkerBuildFlavorDirectory
}

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

    //所有tinker相关的参数配置
    tinkerPatch {

        oldApk = getOldApkPath() //指定old apk文件路径

        ignoreWarning = false   //不忽略tinker的警告,有警告则中止patch文件的生成

        useSign = true  //强制patch文件也使用签名

        tinkerEnable = buildWithTinker(); //指定是否启用tinker

        buildConfig {

            //applyMapping = getApplyMappingPath()  //指定old apk打包时所使用的混淆文件

            applyResourceMapping = getApplyResourceMappingPath()  //指定old apk的资源文件

            tinkerId = getTinkerIdValue() //指定TinkerID

            keepDexApply = false
        }

        dex {

            dexMode = "jar" //jar、raw
            pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex文件目录
            loader = ["androidjian.tinker.MyTinkerApplication"] //指定加载patch文件时用到的类
        }

        lib {

            pattern = ["libs/*/*.so"]
        }

        res {

            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            //指定tinker可以修改的所有资源路径

            ignoreChange = ["assets/sample_meta.txt"] //指定不受影响的资源路径

            largeModSize = 100 //资源修改大小默认值
        }

        packageConfig {

            configField("patchMessage", "fix the 1.0 version's bugs")

            configField("patchVersion", "1.0")
        }
    }

    //判断当前是否配置多渠道
    List flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0

    /**
     * 复制基准包和其它必须文件到指定目录
     */
    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

                        if (variant.metaClass.hasProperty(variant, 'packageApplicationProvider')) {
                            def packageAndroidArtifact = variant.packageApplicationProvider.get()
                            if (packageAndroidArtifact != null) {
                                from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().apkData.outputFileName)
                            } else {
                                from variant.outputs.first().mainOutputFile.outputFile
                            }
                        } else {
                            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")
                        }
                    }
                }
            }
        }
    }
}

测试TinkerDemo

E:\xxx\TinkerDemo\app\src\main\java\com\example\tinkerdemo\MainActivity.java

    public void startTinkerUpdate(View view) {
        File downloadCacheDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

        String path  = downloadCacheDirectory.getAbsolutePath()+File.separator+"patch_signed.apk";
        Log.d(TAG, "startTinkerUpdate: "+path);
        TinkerManager.loadPatch(downloadCacheDirectory.getAbsolutePath()+File.separator+"patch_signed.apk");
    }

没有使用tinker之前的效果
小试牛刀 --- Tinker集成教程_第1张图片接下来修改一下需要解决的问题, 这里我在布局文件里修改一些东西

对应的需要记住 这个版本的apk 以及 resource R文件,作为基准包。 如果你添加了混淆,需要添加对应的mapping文件。
小试牛刀 --- Tinker集成教程_第2张图片小试牛刀 --- Tinker集成教程_第3张图片小试牛刀 --- Tinker集成教程_第4张图片
小试牛刀 --- Tinker集成教程_第5张图片小试牛刀 --- Tinker集成教程_第6张图片
得到差分包以后,就可以上传到服务器供用户下载,更新。 这里我就直接拷贝到手机的指定目录。

小试牛刀 --- Tinker集成教程_第7张图片至此,我们的热更新就已经完成了。上面的图我们可以发现,差分包其实就只有几kb大小。 用户只需很短的时间就可以下载好更新包。

遇到的坑

  • 权限问题
    加载apk需要将apk复制到指定的文件目录进行资源 和dex文件的插入替换。需要SDK读写权限。 如果你是网络更新还需要网络权限。
  • android9.0问题
    用真机测试发现出现这样的bug
Tinker.DefaultLoadReporter: tinker load exception   ensureStringBlocks []

原因是ensureStringBlocks 已经被加入到黑名单,搜索 github tinker issues ,因为9.0原因,建议使用最新的tinker版本

  • 生成差分包处所
    https://github.com/Tencent/tinker/issues/961
    详细原因查看这个issues
Execution failed for task ':app:tinkerProcessReleaseResourceId'. > java.io.FileNotFoundException: build\intermediates\tinker_intermediates\values_backup

解决办法:
1.基准文件备份下
2.clean项目clean
3.打补丁包

github Demo地址

你可能感兴趣的:(android)