有赞零售 App 离线切换技术方案

一、离线模式的价值

有赞零售客户端的用户是需要经营线下门店的商家,在商家的经营时间内,如果软件不能保证正常使用会导致经营效率下降,甚至客户流失。因此除了不断优化软件性能,降低崩溃率,还要做好异常情况的降级处理,比如遇到网络故障和服务器故障等情况时,软件要保证核心功能的可用性,此时软件的工作模式被称为离线模式。

在离线模式下,客户端不能和服务端进行正常的数据通信,所有的数据存储和计算逻辑都要客户端独立完成。目前有赞零售客户端在离线模式下支持登录、收银支付、订单管理、会员积分、部分营销活动等核心功能,即使在极端情况下,有赞零售客户端依然保证商户的经营活动正常进行。

在客户端离线解决方案中主要有两个问题:

1、如何准确及时的触发或退出离线模式。

2、离线场景下的各个业务如何进行数据处理和同步。

本文针对第一个问题,以 iOS 零售客户端为例对其技术实现细节展开分析。

二、离线切换的技术实现

首先我们要清楚商家在什么场景下需要切换到离线模式。试想一下这样的场景:

小明是一名门店收银员,此时正是店里的客流高峰期,收银机前等待结账的顾客已经排起了队,不凑巧的是,此时店里的网络信号很不稳定,开单会等待很久才能有结果,有时会提示网络超时,收银效率很低,排队等待的顾客开始有些不耐烦了,小明只能一边安抚顾客,一边尝试着进行开单收银操作。后来网络彻底断掉,软件无法进行开单操作了,排队的顾客放下准备买的东西离开了,小明只能干等着技术人员来修复网络。

如果以上场景,收银软件能够切换成离线模式,脱离对网络的依赖,确保现有的顾客都能顺利进行结账,收银效率和顾客体验都不会受到影响。此外也存在服务器出现故障的情况,导致客户端的数据请求失败,此时也需要切换到离线模式。因此有赞零售客户端设计了两种切换离线的功能:手动切换能力和针对断网和服务故障的自动切换能力。

为此我们设计一个离线模块用来实现离线模式的触发和退出,它位于业务层和网络层的中间。业务层中各业务模块通过依赖注入的方式获取离线的状态变化和原因,而离线场景下的具体功能由各业务模块实现。业务层通过网络层发送业务数据请求,如果返回的数据出现异常,网络模块会将错误分别发送给业务层和离线模块,离线模块分析接口信息和返回的数据,进而启动服务故障识别功能。

离线模块主要提供三个能力:

  • 离线状态管理
  • 网络故障检测
  • 核心服务故障检测

2.1 离线状态管理

是否处于离线状态是由三个因子共同决定的:

  • 标记离线
  • 网络故障
  • 服务故障

其中标记离线是用户想要主动启动离线模式时,点击指定按钮触发离线模式。网络故障和服务故障是触发离线状态的另外两个条件。我们通过有限状态机维护离线状态的变化,只有当标记离线、网络故障和服务故障的状态都是 False,才会恢复在线状态,否则一直是离线状态。

标记离线通过本地文件缓存来保存状态,因此软件崩溃并不会影响离线状态的管理。对于网络故障,细分为蜂窝移动网络故障还是 WIFI 故障。服务故障解析是根据报错的接口判断出是哪个业务域出的问题。网络故障和服务故障都是实时监测获取状态,具体的监测手段会在下文展开介绍。

2.2 网络故障检测

要实现网络状态的实时监测,我们首先考虑采用苹果官方提供的 Reachability 库,Reachability 的实现依赖于系统提供的 SCNetworkReachability 类,SCNetworkReachability 允许应用程序获取当前系统的网络配置情况,包括网络类型和当前的连接状态。

Reachability 的使用分为同步模式和异步模式。在同步模式下,应用程序主动调用 Reachability 的 currentReachabilityStatus 方法获取当前的网络连接状态。

复制代码

currentReachabilityStatus
{
    NSAssert
(_reachabilityRef 
!= NULL
, 
@"currentNetworkStatus called with NULL SCNetworkReachabilityRef"
);
    NetworkStatus
 returnValue =
 NotReachable
;
    SCNetworkReachabilityFlags
 flags;
    // 同步模式获取网络连接状态
    if
 (
SCNetworkReachabilityGetFlags(
_reachabilityRef,
 &
flags))
    {
        returnValue =
 [
self networkStatusForFlags
: flags
];
    }

    return
 returnValue;
}

在异步模式下,应用程序需要调用 Reachability 的 startNotifier 方法开启一个 RunLoop 去监听网络状态的变化,当网络状态发生变化时,Reachability 会执行 ReachabilityCallback 回调函数,在这个回调函数里会发送 kReachabilityChangedNotification 通知,应用程序监听这个通知就可以感知网络状态的变化。源码如下:

复制代码

- 
(BOOL
)startNotifier
{
    BOOL returnValue =
 NO;
    SCNetworkReachabilityContext
 context =
 {
0,
 (
__bridge void
 *)(
self),
 NULL,
 NULL,
 NULL};
    // 构造一个监听网络连接状态的上下文信息,详细说明见下面

    // 通过调用 SCNetworkReachabilitySetCallback 函数(并传入 Reachability 对象的 ref,以及根据 SCNetworkReachabilityCallBack 自定义的一个回调函数和上述 context)设置 ref 的网络连接状态变化时对应的回调函数为 ReachabilityCallback
    if
 (
SCNetworkReachabilitySetCallback(
_reachabilityRef,
 ReachabilityCallback
, 
&context
))
    {
        // 通过调用 SCNetworkReachabilityScheduleWithRunLoop 函数设置 Reachability 对象的 ref 在 Current Runloop 中对应的模式(kCFRunLoopDefaultMode)开始监听网络状态
        if
 (
SCNetworkReachabilityScheduleWithRunLoop(
_reachabilityRef,
 CFRunLoopGetCurrent
(), kCFRunLoopDefaultMode
))
        {
            returnValue =
 YES;
        }
    }

    // 如果监听成功,返回 YES,否则返回 NO
    return
 returnValue;
}

...
static 
void 
ReachabilityCallback(
SCNetworkReachabilityRef target
, 
SCNetworkReachabilityFlags flags
, 
void*
 info)
{
#pragma unused 
(target
, flags
)
    NSCAssert
(info 
!= NULL
, 
@"info was NULL in ReachabilityCallback"
);
    NSCAssert
([(__bridge 
NSObject*)
 info isKindOfClass:
 [
Reachability 
class]],
 @
"info was wrong class in ReachabilityCallback");

    Reachability
* noteObject 
= 
(__bridge 
Reachability 
*)info
;
    // 因为上述 context 传入的是 self(Reachability 对象),所以这里的 info 为 Reachability 对象类型

    // 发送一个全局通知告诉监听者网络连接状态已发生改变,可通过 noteObject 获取状态
    [[
NSNotificationCenter defaultCenter
] postNotificationName
: kReachabilityChangedNotification 
object:
 noteObject];
}

然而 Reachability 存在一个缺陷,当应用程序可以将一个数据包发出时,SCNetworkReachability 就认为网络是可达的,但是这个数据包是否到达目标地址,SCNetworkReachability 并不清楚,所以它并不能真正检测服务可达性。

于是我们采用一个三方开源库 RealReachability 的方案,这个库是在 Reachability 的基础上进行改进,借助系统的 Socket 库实现的 ping 功能,通过不断对目标地址发送 ping 请求,根据返回的结果判断目标地址是否可达,因而可以更准确的识别当前的网络状态。

在实际应用中,会遇到网络状况时好时坏的情况,RealReachability 的方案会造成业务层频繁地在离线模式和正常模式间来回切换,影响用户体验,有的场景下甚至会导致反复刷新页面,进而引起卡顿。为了解决这个问题,我们在 RealReachability 上再一次进行优化,加入网络防抖功能,它的机制是网络状态的变化不会实时影响离线状态,而是设置一个时间缓冲值 T1,当网络断开时,我们会等待 T1 时间再检查网络是否断开,如果此时网络已经恢复,不会触发离线模式,如果此时网络仍然是断开的,就触发离线模式。在这个缓冲时间内的网络反复变化不会影响离线状态,因此就不会造成频繁的离线切换。

此外 RealReachabilityping 检测的时间间隔为 T2,当发现网络连接断开时,为了及时地检测到网络的恢复,我们会以更快的频率进行 ping 操作,网络恢复后检测的时间间隔也会恢复到 T2。

2.3 服务故障检测

服务故障的检测流程如下图所示:

第一阶段是网络层错误分发,业务层通过网络层发送请求给相应的服务端,当返回的数据出现异常,网络层会启动错误处理的流程,一方面会把错误信息返回给业务层,另一方面把错误信息和请求接口信息一起发给离线模块。

第二阶段是错误信息的本地解析,离线模块维护一份核心业务白名单,这里会配置客户端使用的各个核心业务的接口信息,根据这个白名单可以判断请求的接口所属的业务领域,如果当前报错的接口命中了白名单,离线模块会认为核心业务存在故障风险。

第三阶段是服务端的故障检测,离线模块会请求 QoS 智能决策系统,它根据不同业务后端系统的报警信息来判断是否发生服务故障。如果它判断发生了服务故障,离线模块会收到通知,进而更新本地的离线状态。

第四阶段是故障恢复的检测,离线模块会启动后台线程定期去轮询 QoS 智能决策系统,直到它判断服务故障已经恢复,停止轮询并更新离线状态。

三、展望

本文介绍了零售客户端离线切换解决方案,接下来的问题是业务层如何进行数据处理和同步,以收银开单流程为例,涉及到账号、商品、营销、会员、支付、订单等多个业务模块,各个模块在离线状态下如何存储和处理数据,保证用户在离线状态下可以完成收银功能,以上这些问题我们会在后续文章里详细介绍,敬请期待。

本文转载自公众号有赞 coder(ID:youzan_coder)。

原文链接:

https://mp.weixin.qq.com/s/ZuQ85PnhOW94m0U4_Yk_2w