阅读 351

基于《阿里Sophix》的热修复总成方案

从初次引进Sophix到现在已经过去快一年了,但是实际应用却不多,总结了一下原因:

1.由于一开始的不仔细,我写的逻辑判断有bug,导致只有初次启动能请求热修复加载接口,因此错过了一次难得的体验机会;

2.引进热修复的项目在过去大半年的需求很少,我们需要需求更频繁的项目上应用;

3.一开始的逻辑其实很简陋,就是启动时基于时间限制的接口请求,实际使用可能需要用户多次杀进程,效果很难体现出来;

逻辑优化

1.原时间限制->基于版本的时间限制,版本更新,时间清零重算;(阿里热修复的收费标准之一是接口请求次数,所以必须做限制)

2.原触发条件只有【启动时】->新增【切到后台且APP当前页是首页】触发条件;(保证【APP当前页是首页】可以避免用户正在进行的操作被中断)

3.新增自动杀死逻辑,当新的补丁包下载完成,应用切到后台且APP当前页是首页,连续5s都在后台,则杀进程,用户下次启动时补丁包加载,bug被修复

4.除了【BuilType】、新增【Android系统版本】、【手机厂商】、【手机型号】等三个tag,方便管理

技术细节

从ApplicationInfo中获取DEBUG状态

我们在初始化热修复的时候,需要打上不同的tag,以便灵活进行灰度发布,

然而初始化的类不允许引用任何非Android SDK 的类,是的,连BuildConfig也不允许,

这样一来,就得额外维护一个DEBUG变量,其实很不安全;

通过对apk得解压发现,release和debug版本得AndroidManifest.xml中的application标签下的配置有所不同,

之后查了资料,发现另外一种直接从applicationInfo获取DEBUG状态的方法:

boolean isDebug = getApplicationInfo() != null &&
                (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
复制代码
<!--测试包-->
<application
        android:theme="@ref/0x7f0f000f"
        android:label="@ref/0x7f0e0071"
        android:icon="@ref/0x7f0c00bb"
        android:name="cn.com.bluemoon.wash.SophixStubApplication"
        android:debuggable="true"
        android:allowBackup="false"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"
        android:networkSecurityConfig="@ref/0x7f110002"
        android:appComponentFactory="android.support.v4.app.CoreComponentFactory">
<!--正式包-->
<application
        android:theme="@ref/0x7f0f000f"
        android:label="@ref/0x7f0e0071"
        android:icon="@ref/0x7f0c00bb"
        android:name="cn.com.bluemoon.wash.SophixStubApplication"
        android:allowBackup="false"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"
        android:networkSecurityConfig="@ref/0x7f110002"
        android:appComponentFactory="android.support.v4.app.CoreComponentFactory">
复制代码

基于版本的时间限制

fun queryAndLoadNewPatchExamine(context: Context) {
        val perf = createSharedPreferences(SOPHIX_SHAREDPREFERENCES_NAME,
                context)
        val versionKey = BuildConfig.VERSION_NAME//使用版本名称作为key去存储上一次请求接口的时间
        val currentTime = System.currentTimeMillis()
        val lastTime = getLong(perf, versionKey)
        if (lastTime == 0L || currentTime - lastTime > TIME_INTERVAL) {
            //4.2及以上系统以上才支持SystemClock.elapsedRealtimeNanos(),在此之前用System.currentTimeMillis()
            putLong(perf, versionKey, currentTime)
            LogUtils.d("****test****", "SophixManager.getInstance().queryAndLoadNewPatch()");
            SophixManager.getInstance().queryAndLoadNewPatch()
        }
    }
复制代码

【热修复】的三个状态

当我们发布一个补丁包时,我们关注三个状态——通知成功、下载成功、加载成功,

我们由从中抽象出两个状态,在下载成功之前,我们统称 【待请求】

这个状态下我们会尽量去请求询问是否有补丁包,

当下载成功但是未加载时,我们统称 【待加载】

从实际工程上考虑我们只需要维护这两个抽象出来的状态。

【待请求】和【待加载】的状态切换

默认情况下(APP启动时初始化),状态都是【待请求】,

只有当补丁包下载完成时,才会切换成【待加载】,并且尝试去杀死应用,

又由于只有两种状态,实际上我是用一个布尔值来维护的。

//下载完成时的回调
public void loadRelaunch() {
        isLoadRelaunch = true;//切换状态
        if (activityCount == 0) {//尝试杀死应用
            handler.sendEmptyMessageDelayed(WHAT, TIME);
        }
    }
复制代码

什么叫做连续5s在后台?

我们杀死应用的条件之一时连续5s在后台,但是具体实现呢?

事实上,我是用handle延时处理消息,以500ms为一个计数,共计10次,累计约等于5s,

实际上这个时间是不准确的,但是并不影响我们的逻辑,实际工程实现也并无大碍。

@Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //静默5s之后如果还在后台且满足条件就杀进程
            if (ActivityManager.getInstance().getLastActivity() != null
                    && ActivityManager.getInstance().getLastActivity() instanceof MainTabActivity
                    && SophixActivityLifecycleImpl.getInstance().activityCount == 0) {
                if (num >= MAX_TIME) {
                    //连续5s都是在后台
                    //判断最上层的activity是否是singleTask的MainTabActivity,并且此时,前台Activity个数为0,
                    //杀进程
                    ActivityManager.getInstance().finishAllActivity();
                    SophixManager.getInstance().killProcessSafely();
                } else {
                    //500ms数一次
                    num += TIME;
                    sendEmptyMessageDelayed(WHAT, TIME);
                }
            } else {
                num = 0;
            }
        }
复制代码

测试集成的痛点

热修复的工作中,最令人感到厌烦的就是为了模拟热修复的场景:

我们需要模拟实际场景给代码加bug,先后打出多个包,做差异包进行测试。

这个过程不仅会带来需要多次编译打包的繁琐,而且不可靠。

热修复两个包之间有一些东西是不能改的,比如入口的SophixStubApplication,比如AndroidManifest

如果这些地方不小心被修改,轻则补丁包生成失败,重则会产生其他bug对测试造成干扰。

多源集构建

我们知道在gradle中配置builType可以构建debug和release两种包,事实上我们还可以通过配置productFlavors从另一个维度去同时构建多个apk:

flavorDimensions 'hotfix'

    productFlavors {
        before {
            dimension 'hotfix'
        }

        current {
            dimension 'hotfix'
        }

        after {
            dimension 'hotfix'
        }
    }
复制代码

然后在多个源集中给配置不同资源,或者不同代码的逻辑,这样一次性出三个不同包,

既能确保需要保持不变的地方是一致的,也能灵活配置模拟各种bug情况,可以说是一举多得,

这也是我学习【多源集构建】以来遇见的最合适的应用场景;

总结

以上就是本次优化的热修复逻辑,从目前看来,它已经称得上是一套成熟的解决方案了。

关注下面的标签,发现更多相似文章
评论