Android开发 热修复Tinker框架接入、发布流程及问题解决

4,158 阅读10分钟

Tinker的作用

作为移动端开发者,时常会遇到因为一些小bug而需要重新发版的问题,这还属于小问题,如遇到一些大问题的话,若是通过用户更新来修复那效率就太慢了,亦或是遇到不更新的用户,那么该bug将一直存在用户手机上,这是非常危险的。Tinker是为了解决这种问题而生的,修改少量的代码,生成差分包,然后用户无感下载非常小的更新包,就可以解决问题。它是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。

当然,你也可以使用Tinker来更新你的插件。虽然Tinker可以替代更新,但是为了适应单一职责,因此我还是建议如果需要更新功能的话还是直接发版本,如果是修复一些小bug,那么就可以使用Tinker。

Tinker源码Github地址

接入(亲历心路历程)

找文档

首先打开Github地址,找到底部接入指南。OK,无中文版咋整?(后来发现有中文的文档,只是在他的github主页没有附上)
通过其他大牛和博主了解到,要接入Tinker且使用它,必须要还要接入Bugly。(WTF?)
看看其他热更新框架算了。
结果发现几个主流的全都停止更新了,只有Tinker还在持续更新中。为了可持续发展,没办法,只能接入Tinker了。
但是Tinker的接入文档跟没有一样,那就先看看Bugly如何接把。打开Bugly官网后发现了新大陆,原来Tinker的接入流程全在Bugly的官方文档里面。 这给我激动的差点哭了。

Bugly官网地址

Bugly热更新文档地址 接入Tinker时请拿本文与官方文档配合使用

开始接入

1、在Bugly官网新建我的产品并记录好AppId

QQ图片20211028143120.png

image.png

2、添加插件依赖

工程根目录下“build.gradle”文件中添加:

buildscript {
    repositories {
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath "com.tencent.bugly:tinker-support:1.1.5"
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.14.6')
    }
}

在app module的“build.gradle”文件中添加(注意signingConfigs的注释):

apply plugin: 'com.android.application'
// tinker依赖的插件脚本
apply from: 'tinker-support.gradle'

android {
    //必须配置这个,否则打补丁包的时候会因为没有签名而报错
    signingConfigs {
        release {
            storeFile file('D:\XXX.jks')
            storePassword 'XXX'
            keyAlias 'XXX'
            keyPassword 'XXX'
        }
        debug {
            storeFile file('D:\XXX.jks')
            storePassword 'XXX'
            keyAlias 'XXX'
            keyPassword 'XXX'
        }
    }
    
    defaultConfig {
        multiDexEnabled true
        ndk {
            abiFilters "arm64-v8a", "armeabi-v7a"
        }
    }

    // recommend
    dexOptions {
        jumboMode = true
    }

    buildTypes {
        release {
            // 混淆
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            buildConfigField("boolean", "LOG_ERROR", "false")
            // 注意此处必填
            signingConfig signingConfigs.release
        }
        debug {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            buildConfigField("boolean", "LOG_ERROR", "true")
            // 注意此处必填
            signingConfig signingConfigs.debug
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}

dependencies {
    // tinker
    implementation "com.android.support:multidex:1.0.1" // 多dex配置
    implementation 'com.tencent.bugly:crashreport_upgrade:1.3.6'
    implementation 'com.tencent.tinker:tinker-android-lib:1.9.14.6'
}

如果此时同步会报错,因为tinker-support.gradle文件不存在,这个文件才是我们打包的主要文件,当我们项目接入Tinker后,如果临时不需要使用Tinker的话,注释apply from: 'tinker-support.gradle' 这段代码就行了。

3、新建tinker-support.gradle

image.png

apply plugin: 'com.tencent.bugly.tinker-support'

def bakPath = file("${buildDir}/bakApk/")//这里指向的就是app/bulid/bakApk目录
def baseApkDir = "app-release-folder" //此处填写每次构建生成的基准包目录
//def myTinkerId = "base-" + rootProject.versionName // 用于生成基准包(不用修改)
def myTinkerId = "patch-" + rootProject.versionName + ".0" // 用于生成补丁包(每次生成补丁包都要修改一次,最好是 patch-${versionName}.x.x)


tinkerSupport {
    // 开启tinker-support插件,默认值true
    enable = true

    // 指定归档目录,默认值当前module的子目录tinker
    autoBackupApkDir = "${bakPath}"

    // 是否启用覆盖tinkerPatch配置功能,默认值false
    // 开启后tinkerPatch配置不生效,即无需添加tinkerPatch
    overrideTinkerPatchConfiguration = true

    // 编译补丁包时,必需指定基线版本的apk,默认值为空
    // 如果为空,则表示不是进行补丁包的编译
    // @{link tinkerPatch.oldApk }
    baseApk = "${bakPath}/${baseApkDir}/app-release.apk"

    // 对应tinker插件applyMapping
    baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt"

    // 对应tinker插件applyResourceMapping
    baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt"

    // 是否启用加固模式,默认为false.(tinker-spport 1.0.7起支持)
    // isProtectedApp = false

    // 是否开启反射Application模式
    enableProxyApplication = true

    // 是否支持新增非export的Activity(注意:设置为true才能修改AndroidManifest文件)
    supportHotplugComponent = true

    // 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
    tinkerId = "${myTinkerId}"

    // 构建多渠道补丁时使用
    // buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
}


tinkerPatch {
    tinkerEnable = true
    ignoreWarning = false
    useSign = true
    dex {
        dexMode = "jar"
        pattern = ["classes*.dex"]
        loader = []
    }
    lib {
        pattern = ["lib/*/*.so"]
    }

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

    packageConfig {
    }
    sevenZip {
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
    }
    buildConfig {
        keepDexApply = false
    }
}

这里有几个点需要注意一下:

  • 这个文件必须在app里面,与app的build.gradle同级
  • 只有开启了enableProxyApplication = true,那么注释apply from: 'tinker-support.gradle'就会让Tinker停用,如果是按照官方的文档重写application的话,那么enableProxyApplication要等于false,那么想停用Tinker的话还需要去处理application。
  • bakPath、baseApkDir、myTinkerId三个参数的值在打包的时候有重要作用,这里注意一下就行,后面会有打包发布的操作流程。

4、初始化Bugly+Tinker

方式一:反射MyApplication

public class MyApplication extends Application {
    private static MyApplication appContext;
    @Override
    public void onCreate() {
        super.onCreate();
        appContext = this;
        configTinker();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        // you must install multiDex whatever tinker is installed!
        MultiDex.install(getApplication());
        // 安装tinker 此接口仅用于反射Application方式接入。
        Beta.installTinker();
    }

    public static synchronized MyApplication getApplication() {
        return appContext;
    }

    /**
     * 初始化Tinker
     */
    private void configTinker(){
        //是否开启热更新能力
        Beta.enableHotfix = true;
        //是否开启自动下载补丁
        Beta.canAutoDownloadPatch = true;
        //是否自动合成补丁
        Beta.canAutoPatch = true;
        //是否提示用户重启
        Beta.canNotifyUserRestart = false;
        //补丁回调接口
        Beta.betaPatchListener = new BetaPatchListener() {
            @Override
            public void onPatchReceived(String s) {
                LogUtil.e(TAG, "补丁下载地址:" + s);
            }

            @Override
            public void onDownloadReceived(long l, long l1) {
                LogUtil.e(TAG, String.format(Locale.getDefault(), "%s %d%%",
                        Beta.strNotificationDownloading,
                        (int) (l1 == 0 ? 0 : l * 100 / l1)));
            }

            @Override
            public void onDownloadSuccess(String s) {
                LogUtil.e(TAG, "补丁下载成功");
            }

            @Override
            public void onDownloadFailure(String s) {
                LogUtil.e(TAG, "补丁下载失败");
            }

            @Override
            public void onApplySuccess(String s) {
                LogUtil.e(TAG, "补丁应用成功");
            }

            @Override
            public void onApplyFailure(String s) {
                LogUtil.e(TAG, "补丁应用失败");
            }

            @Override
            public void onPatchRollback() {

            }
        };

        //第二个参数true表示是开发设备,在Bugly的后台发布补丁时,可以选择全部用户还是开发设备
        Bugly.setIsDevelopmentDevice(getApplication(), true);
        // 多渠道需求塞入
        // String channel = WalleChannelReader.getChannel(getApplication());
        // Bugly.setAppChannel(getApplication(), channel);
        // 这里实现SDK初始化,appId替换成你的在平台申请的appId isDebug是否是调试模式
        Bugly.init(getApplication(), appId, isDebug);
    }
}

方式二:不反射,官方建议的方式,该方式会增加接入成本,但有更好的兼容性

1、“tinker-support.gradle”文件中设置“enableProxyApplication = false”;

2、自定义Application,注意,继承TinkerApplication

public class MyApplication extends TinkerApplication {
    // 此文件只写这段代码,其他代码统一全部放到MyApplicationLike里面去
    public MyApplication() {
        super(ShareConstants.TINKER_ENABLE_ALL, "xx.xx.xx.MyApplicationLike",
                "com.tencent.tinker.loader.TinkerLoader", false);
    }
}

3、自定义ApplicationLike

public class MyApplicationLike extends DefaultApplicationLike {

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

    @Override
    public void onCreate() {
        super.onCreate();
        configTinker();
            
    }

    @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);

        // 安装tinker
        Beta.installTinker(this);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
        getApplication().registerActivityLifecycleCallbacks(callbacks);
    }

    /**
     * 初始化Tinker
     */
    private void configTinker(){
        //是否开启热更新能力
        Beta.enableHotfix = true;
        //是否开启自动下载补丁
        Beta.canAutoDownloadPatch = true;
        //是否自动合成补丁
        Beta.canAutoPatch = true;
        //是否提示用户重启
        Beta.canNotifyUserRestart = false;
        //补丁回调接口
        Beta.betaPatchListener = new BetaPatchListener() {
            @Override
            public void onPatchReceived(String s) {
                LogTools.e(TAG, "补丁下载地址:" + s);
            }

            @Override
            public void onDownloadReceived(long l, long l1) {
                LogTools.e(TAG, String.format(Locale.getDefault(), "%s %d%%",
                        Beta.strNotificationDownloading,
                        (int) (l1 == 0 ? 0 : l * 100 / l1)));
            }

            @Override
            public void onDownloadSuccess(String s) {
                LogTools.e(TAG, "补丁下载成功");
            }

            @Override
            public void onDownloadFailure(String s) {
                LogTools.e(TAG, "补丁下载失败");
            }

            @Override
            public void onApplySuccess(String s) {
                LogTools.e(TAG, "补丁应用成功");
            }

            @Override
            public void onApplyFailure(String s) {
                LogTools.e(TAG, "补丁应用失败");
            }

            @Override
            public void onPatchRollback() {

            }
        };

        //第二个参数true表示是开发设备,在Bugly的后台发布补丁时,可以选择全部用户还是开发设备
        Bugly.setIsDevelopmentDevice(getApplication(), true);
        // 多渠道需求塞入
        // String channel = WalleChannelReader.getChannel(getApplication());
        // Bugly.setAppChannel(getApplication(), channel);
        // 这里实现SDK初始化,isDebug:是否是调试模式
        Bugly.init(getApplication(), "APP ID", false);
    }
} 

AndroidManifest文件只管权限就行了,1.3.1版本以上已经兼容了FileProvider,混淆参考官方文档,这里不做赘述。至此,Tinker接入完毕。

发布流程

首先我们需要了解两个名词

  • 基准包:我们发布到线上的apk,用户正常使用的包
  • 补丁包:基于基准包所产生的差异包,修复了基准包里面的bug,使用基准包的用户可以下载补丁包通过打补丁的方式来修复bug

实行热修复

如果发现线上问题,经过评估应该利用热修复(而不是app升级)来解决的,在我们修复bug文件之后,按照如下流程:

1. 将生产包apk(生成包apk和mapping文件必须在打包的时候备份好)放在工程内的下面位置,这个目录地址必须是tinker-support.gradle文件里面的变量bakPath+baseApkDier的目录。

这个目录需要自己新建而不是Tinker自动生成,这里需要注意一下。我一开始以为是Tinker会自动生成,导致我打补丁包的时候一直报错。

image.png

image.png 这是基准包apk和mapping文件存放的完整路径,我们基准包必须放在这个目录下且名字必须是app-release.apk,当然你如果想用别的名字也可以修改baseApk变量的值。mapping同理。

image.png

2. 修改tinkerId作为补丁包的版本号,通常在"patch-" + rootProject.versionName + ".0" 的基础上+1,最终写成: "patch-" + rootProject.versionName + ".1", 如果打了多次基准包,就在后面加数字区分: "patch-" + rootProject.versionName + ".2"

3. 双击下面的命令,执行生成补丁的过程

image.png

4. 如果正常生成补丁,到如下位置去寻找补丁包,名字为:patch_signed_7zip.apk

image.png

5. 上传补丁包到bugly后台(目标版本是versionName+versionCode组合,是Bugly自己给我们的基准包定义的版本,与TinkerId无关):

image.png

多渠道打包

1、采用在Gradle添加productFlavors的方式

具体接入可以直接参考Tinker的文档
但是该方式对打基准包和补丁包,都需要消耗大量的时间

2、采用第三方框架Walle集成多渠道打包的方式(本人采用)

Walle源码地址,里面附接入流程
接入完成之后,我们通过执行命令gradlew clean assembleReleaseChannels进行多渠道打包
打包成功的话会在命令控制台自动输出BUILD SUCCESSFUL,并且在该图片对应目录生成对应的渠道包

image.png 并且会在build/bakApk目录下面生成app-release.apk,这个包就可以作为我们的基准包,我们需要备份好,日后需要打补丁包的时候用的上,利用这个多渠道打包就可以只生成一个补丁包然后对应所有的渠道,使用时比方式一要方便许多。只是增加了接入成本。

疑难杂症解决办法

1、高版本Gradle兼容问题:

我目前用的是3.6.4版本,4.0以上的版本好像有兼容问题,不知道现在修复没,所以说,如果工程有别的工程对Gradle有要求且和Tinker冲突,那就GG了。

2、清单文件<queries>标签的适配:

Android11需要配置配置该标签用于跳转到第三方应用,我们知道gradle版本4.1+是可以适用的,如果低于4.1的话,那么gradle选择的版本应该是4.0.1/3.6.4/3.5.4/3.4.3/3.3.3

3、基准包使用和打包补丁包时可能出现的问题:

1、使用的基准包必须是自己打包的release包,而不是tinker在build/bakApk目录下自动生成的包
2、每次打包发布apk得时候,apk和mapping必须备份好,因为不同的版本可能会出现不同的bug,
   那么我们修复时用到的基准包也会不一样
3、在打补丁包时候,基准包的路径必须正确,否则会找不到基准包而打包失败
4、必须在app的build.gradle里面配置签名文件,否则打包时会因为找不到签名文件而失败
5、每次修改补丁包必须修改tinkerId,否则会导致打补丁出错
6、同一个基准包下,发布了很多补丁包,建议把老补丁包撤销一下,否则用户可能会出现补丁混乱的问题

4、Tinker与viewbinding冲突的问题:

需要将Tinker升级到v1.9.14.6版本且在工程的build.gradle里面加入:
classpath('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.14.6')

5、兼容google play

参考https://github.com/Tencent/tinker/issues/1314

6、打补丁包错误:loader classes are found in old secondary dex.Found classes:XXX

在tinker-support.gradle的tinkerSupport里面加入tinkerEnable = true,ignoreWarning = true

也可以尝试tinkerpatch.gradle配置文件中 在tinkerPatch下面添加
allowLoaderInAnyDex = true
removeLoaderForAllDex = true

若还是无法处理,可以参考tinker官网该问题的处理(https://github.com/tencent/tinker/issues/104)

7、无法修改AndroidManifest.xml文件

目前Tinker的版本,Android系统10以上不能新增四大组件,低于10的系统可以新增activity。