Android-Tinker 腾讯bugly热修复踩坑

本知识点只是个人见解,具体知识及使用请查阅官网,以免被误导,同时大家可以对此文发表自己的见解。


不得不说,接入第三方的时候,稍微不注意,坑就很多! 所以要注意很多细节在里面!

之前写了个适配8.0的方案 https://blog.csdn.net/yang1349day/article/details/80016607 后来适配的问题还是很多,于是接了腾讯的bugly.使app能够在线去修复bug.

不得不说bugly的接入文档写的非常好.

此篇文章只是个人接入过程中的坑.具体大部分的坑见 https://bugly.qq.com/docs/user-guide/faq-android-hotfix/?v=20170912151050


先来看总体的流程:

完整接入流程
  • 打基准包安装并上报联网(注:填写唯一的tinkerId)
  • 对基准包的bug修复(可以是Java代码变更,资源的变更)
  • 修改基准包路径、修改补丁包tinkerId、mapping文件路径(如果开启了混淆需要配置)、resId文件路径
  • 执行buildTinkerPatchRelease打Release版本补丁包
  • 选择app/build/outputs/patch目录下的补丁包并上传(注:不要选择tinkerPatch目录下的补丁包,不然上传会有问题
  • 编辑下发补丁规则,点击立即下发
  • 杀死进程并重启基准包,请求补丁策略(SDK会自动下载补丁并合成)
  • 再次重启基准包,检验补丁应用结果
  • 查看页面,查看激活数据的变化
Github Demo

1 打好path包之后,上传到bugly后台显示如图:


首先你看下这个官方视频http://v.qq.com/boke/gplay/9f3b4b1232819f453becd2356a3493c4_bme000301803d13_5_e0385shcfzm.html  如果你看了,可以免过,继续看下面:

我总结了使用过程中的一些经验:

1 打基准包的时候,设置的tinkerId的时候,优先设置自己app版本号

2 记住设置的版本号一定要比用户正在使用的要大(如果你是第一次接入的话).

比如市场上的版本号是1.0.9,如果市场上的1.0.9版本设置过了基准包为base-1.0.9 那么release的patch包版本号也设置成patch-1.1.0 ,这里可以理解为基准版本已经线上有了

如果市场上的版本是1.0.9但是没有设置过基准包,则先生成基准包为base-1.1.0, 设置好了之后,把这个基准包在手机上面安装一下,然后仔细观察CrashReport的log日志(如果你连log日志都不会看,或者看不到后面是很难去接入的),如果联网成功,bugly后台就会知道你这个基准包的版本了. 然后生成补丁包,生成release的patch包版本号也设置成patch-1.1.0 (此处这么做是为了规范,免得不必要的低级错误)

以上步骤完成,基本上不会出现,基准包没有上传的问题了.

注意: 一定要观察log, 同时最好在application处设置监听,如下代码:

(timber是打印log日志的工具类,详情见 https://github.com/JakeWharton/timber)

private void initializeBugly() {
        Beta.enableHotfix = true;
        // 设置是否自动下载补丁,默认为true
        Beta.canAutoDownloadPatch = true;
        // 设置是否自动合成补丁,默认为true
        Beta.canAutoPatch = true;
        // 设置是否提示用户重启,默认为false
        Beta.canNotifyUserRestart = true;
        // 补丁回调接口
        Beta.betaPatchListener = new BetaPatchListener() {
            @Override
            public void onPatchReceived(String patchFile) {
//                Toast.makeText(getApplication(), "补丁下载地址" + patchFile, Toast.LENGTH_SHORT).show();
                Timber.tag("CrashReport");
                Timber.e("onPatchReceived: "+"补丁下载地址" + patchFile);
            }

            @Override
            public void onDownloadReceived(long savedLength, long totalLength) {
//                Toast.makeText(getApplication(),
//                        String.format(Locale.getDefault(), "%s %d%%",
//                                Beta.strNotificationDownloading,
//                                (int) (totalLength == 0 ? 0 : savedLength * 100 / totalLength)),
//                        Toast.LENGTH_SHORT).show();
                Timber.tag("CrashReport");
                Timber.e("onDownloadReceived: "+"补丁下载地址" + String.format(Locale.getDefault(), "%s %d%%",
                        Beta.strNotificationDownloading,
                        (int) (totalLength == 0 ? 0 : savedLength * 100 / totalLength)));
            }

            @Override
            public void onDownloadSuccess(String msg) {
//                Toast.makeText(getApplication(), "补丁下载成功", Toast.LENGTH_SHORT).show();
                Timber.tag("CrashReport");
                Timber.e("onDownloadSuccess: "+"补丁下载成功");
            }

            @Override
            public void onDownloadFailure(String msg) {
//                Toast.makeText(getApplication(), "补丁下载失败", Toast.LENGTH_SHORT).show();
                Timber.tag("CrashReport");
                Timber.e("onDownloadFailure: "+"补丁下载失败");
            }

            @Override
            public void onApplySuccess(String msg) {
//                Toast.makeText(getApplication(), "补丁应用成功", Toast.LENGTH_SHORT).show();
                Timber.tag("CrashReport");
                Timber.e("onApplySuccess: "+"补丁应用成功");
            }

            @Override
            public void onApplyFailure(String msg) {
//                Toast.makeText(getApplication(), "补丁应用失败", Toast.LENGTH_SHORT).show();
                Timber.tag("CrashReport");
                Timber.e("onApplyFailure: "+"补丁应用失败");
            }

            @Override
            public void onPatchRollback() {
                Timber.tag("CrashReport");
                Timber.e("onPatchRollback: "+"补丁应用失败");
            }
        };

        /**
         *  全量升级状态回调
         */
        Beta.upgradeStateListener = new UpgradeStateListener() {
            @Override
            public void onUpgradeFailed(boolean b) {
                Timber.tag("CrashReport");
                Timber.e("onUpgradeFailed: "+"全量升级状态回调"+b);
            }

            @Override
            public void onUpgradeSuccess(boolean b) {
                Timber.tag("CrashReport");
                Timber.e("onUpgradeSuccess: "+"全量升级状态回调"+b);
            }

            @Override
            public void onUpgradeNoVersion(boolean b) {
//                Toast.makeText(DidiApplicationLike.this.getApplication(), "最新版本", Toast.LENGTH_SHORT).show();
                Timber.tag("CrashReport");
                Timber.e("onUpgradeNoVersion: "+"全量升级状态回调"+b);
            }

            @Override
            public void onUpgrading(boolean b) {
//                Toast.makeText(DidiApplicationLike.this.getApplication(), "onUpgrading", Toast.LENGTH_SHORT).show();
                Timber.tag("CrashReport");
                Timber.e("onUpgrading: "+"全量升级状态回调"+b);
            }

            @Override
            public void onDownloadCompleted(boolean b) {
                Timber.tag("CrashReport");
                Timber.e("onDownloadCompleted: "+"全量升级状态回调"+b);
            }
        };

        // 设置开发设备,默认为false,上传补丁如果下发范围指定为“开发设备”,需要调用此接口来标识开发设备
        Bugly.setIsDevelopmentDevice(getApplication(), true); //在发布正式版本的时候需要关掉,不然发布新的patch包的时候,测试都没法去测试!
        // 多渠道需求塞入
        // String channel = WalleChannelReader.getChannel(getApplication());
        // Bugly.setAppChannel(getApplication(), channel);

        // 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
        // 调试时,将第三个参数改为true
        Bugly.init(getApplication(), AppConfig.BuglyAppId, AppConfig.ADB);
    }

在测试的时候最好把弹框打开,这样更直观些. 同时把bugly.init第三个参数设置为true,如果没有看到log,请先调试出log后再进行下一步.

当发布的时候,发Appconifg.adb定义为true,即切换到生产环境,而不是开发环境的api(有实际工作经验的都懂的)


2 官网上面展示的是很理想的情况下的bugly配置和修复.但是接入到实际项目可能会有些迷惑如下:

首先来观察下图:


1 目标版本和我们设置的patch版本实际上不一致的哦,不要太纠结. 

2 已激活,已下发之类的其实是有延迟的,所以不要指望会立即生效

3 如果上传版本的时候还是出现未上传基准包,最好先刷新这个页面,让后台即时更新


3 bugly的下载及加载策略

bugly接入完毕,但是有个问题,用户进入app的时候立即闪退,导致bugly下载patch包和合并修复不及时,导致bug无法修改。

所以此处涉及到一个合适加载的问题:

1 参考手Q的策略,就是检测到有patch包就进入下载和加载patch页面,当进度条显示加载完毕则自动重启app进入启动页面,这种效果是非常好的。而官方提供给我的则是一个弹窗,提示用户重启,导致无法及时修复bug。那么怎么去仿手Q的加载页面呢? 那就必须要对回调有所理解,要不断的调试才行,调试的时候要把instans run 关掉才能热启动,不用一直用命令行启动哦。此处还在持续更新....

 2 解决不停闪退的办法还有一个,就是使用uncathExceptionHandler,自定义发生异常之后的操作

可以使用的开源框架是---崩溃日志搜集


4 bugly的实践

实际应用中,请注意保存线上发布版本的基准apk包、mapping文件、R.txt文件,如果线上版本有bug,就可以借助我们tinker-support插件进行补丁包的生成。要是删除了基准包,就无法热更新了哦! 同时,发布新的patch包,调试开发者设备的时候需要设置为false

     // 设置开发设备,默认为false,上传补丁如果下发范围指定为“开发设备”,需要调用此接口来标识开发设备
        Bugly.setIsDevelopmentDevice(getApplication(), true); //在发布正式版本的时候需要关掉,不然发布新的patch包的时候,测试都没法去测试!!!
    

2 如下所示,一个模拟器不停的在刷接口,导致的很多的空指针和异常bug,所以直接可以将其拉黑,限制访问



5 配置生成的apk路径不一致的问题

Demo是直接在build下面生成的apk,不包含渠道路径,和渠道的命名,此处需要外面自己去配置.

这里是我在app目录下build.gradle的配置:

release {
            // 不显示Log
            buildConfigField "boolean", "LOG_DEBUG", "true"
            //混淆
            minifyEnabled true
            //Zipalign优化
            zipAlignEnabled true

            // 移除无用的resource文件
            shrinkResources true

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

            //签名
            signingConfig signingConfigs.release

//            android.applicationVariants.all { variant ->
//                variant.outputs.all {
//                    outputFileName = "YeYouTuan_${variant.versionName}_${variant.productFlavors[0].name}.apk"
//                }
//            }

        }

恩,此处最好是注释掉,不然对于后续tinker-support.gradle 的生成路径是有问题的

然后是渠道的配置:

//渠道Flavors,配置不同风格的app,友盟渠道统计时用到
productFlavors {
    yingyongbao {}
    /*
    "offical" {}
    "360" {}
    yingyongbao {}
    wandoujia {}
    baidu {}
    "91" {}
    */
}

渠道tinker-support.gradle 此处的配置是一致的

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

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

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

此处完成之后如下:


上面的bugly是我存储基准版本的地方,也上传到了svn


最后是我总忘记和混淆的一点

6 普通打包和生成补丁包的步骤

1、编译基准包

配置基准包的tinkerId

配置基准包的tinkerId

tinkerId最好是一个唯一标识,例如git版本号、versionName等等。 如果你要测试热更新,你需要对基线版本进行联网上报。

这里强调一下,基线版本配置一个唯一的tinkerId,而这个基线版本能够应用补丁的前提是集成过热更新SDK,并启动上报过联网,这样我们后台会将这个tinkerId对应到一个目标版本,例如tinkerId = "bugly_1.0.0" 对应了一个目标版本是1.0.0,基于这个版本打的补丁包就能匹配到目标版本。

执行assembleRelease编译生成基准包:

编译基准包

这个会在build/outputs/bakApk路径下生成每次编译的基准包、混淆配置文件、资源Id文件,如下图所示:

生成的基线版本

实际应用中,请注意保存线上发布版本的基准apk包、mapping文件、R.txt文件,如果线上版本有bug,就可以借助我们tinker-support插件进行补丁包的生成

启动apk,上报联网数据

我们每次冷启动都会请求补丁策略,会上报当前版本号和tinkerId,这样我们后台就能将这个唯一的tinkerId对应到一个版本,大家测试的时候可以打开logcat查看我们的日志,如下图所示:

上报联网数据

如果看不到log,您需要将bugly初始化的第三个参数设置为true才能看到

根据基线版本生成补丁包

修改待修复apk路径、mapping文件路径、resId文件路径

配置apk路径、mapping文件路径、resId文件路径

执行构建补丁包的task

执行构建补丁包的task

如果你要生成不同编译环境的补丁包,只需要执行TinkerSupport插件生成的task,比如buildTinkerPatchRelease就能生成release编译环境的补丁包。 注:TinkerSupport插件版本低于1.0.4的,需要使用tinkerPatchRelease来生成补丁包 

生成的补丁包在build/outputs/patch目录下:

build/outputs/patch

大家这里可能会有一个疑问,补丁版本是怎么匹配到目标版本的,可以双击patch包,我们提供的插件会在tinker生成的patch包基础上插入一个MF文件:

tinker-support插件日志

Yapatch.MF文件


最后,需要注意的是,视频一定要看!!! 而且不要边接入边看,要先看完,然后再接入,不动的再回过去看文档或者视频

.同时,入手的时候,最好先自己做个小demo,然后再接进去自己的项目,这是接入所有第三方的基本套路!


展开阅读全文

没有更多推荐了,返回首页