离线与同步

Author Avatar
XibHe 10月 25, 2018
  • 在其它设备中阅读本文章

在弱网环境下,将要面对一些问题:频繁请求网络造成请求超时;完全没有网络导致请求失败。那么,针对这种特殊而又普遍的现象,需要做一些额外的功能去弥补弱网环境
下的操作体验。于是,针对某些重要功能的离线模式就诞生了!

离线功能是为了满足用户在较短时间断网(离线时间最长支持15天)或无法访问服务器的情况下,可以正常的进行部分业务开展;从而保证对核心业务不会造成较大影响。

支持离线模式的模块

  1. 登录
  2. 看病开方
    • 新增处方支持离线
    • 处方列表支持离线
  3. 零售卖药
    • 新增售药单支持离线
    • 售药列表支持离线
  4. 患者管理
    • 新增患者支持离线
    • 患者管理支持离线
  5. 药房管理
    • 新增药品支持离线
    • 药品管理支持离线
  6. 我的
    • 诊所管理支持离线
    • 系统设置支持离线
    • 账号设置支持离线

离线业务的具体步骤

1. 监听当前设备所处的网络状态?

离线模式切换所经历的三个过程:

  1. 提示用户 “失去网络连接,3秒后重试”,之后进入 “网络连接中…” 状态;
  2. 如果重试3次均失败,提示用户 “重新连接失败,进入离线状态”;
  3. 1秒后,提示信息变更为 “因当前网络不可用,您已进入离线状态;离线状态只可使用部分功能!”。

离线状态切换的三种状态,

typedef NS_ENUM(NSInteger,offlineNetType) {
    Off_line,//离线
    In_the_Internet,//联网中
    Network_retry//联网重试
};

监听当前网络的连接状态,若此时未发现可靠网络连接或网络连接失败,则需要经历以上三个阶段,进行离线模式的转换。具体监听步骤如下:

  1. 增加一个单利,并在单利中声明一个 BOOL 值的属性;
@interface Singleton : NSObject

@property(nonatomic,assign)BOOL hasNet;//是否有网络

@end

初始化时设置该属性默认值为 YES,


static Singleton *share = nil;

@interface Singleton()

@end

@implementation Singleton

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.hasNet = YES;
    }
    return self;
}

+ (instancetype)shareInstance 
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
         share = [[Singleton alloc] init];
    });
    return share;
}

@end
  1. 在工具类中增加判断网络

SimplePing 是苹果封装好的关于 ping 的类,用于检测网络的连接状态。SimplePing 是一个封装低层级的 BSD Sockets ping 函数的类。使用这个类创建实例,设置委托然后调用 -start 方法开始在当前的 run loop (运行循环)中。如果顺利的话你很快就会得到 -simplePing:didStartWithAddress: 这个委托的回调。之后在这个回调里你就可以调用 -sendPingWithData: 来发送一个 ping,然后等待接收回调 -simplePing:didReceivePingResponsePacket:sequenceNumber:-simplePing:didReceiveUnexpectedPacket:

首先新建单利类 LZNetworkAvailability,在初始化方法中指定要 ping 的主机名称,

static LZNetworkAvailability *share = nil;
@interface LZNetworkAvailability()<SimplePingDelegate>
@end

+ (void)shareInstanceNetworkAvailability{
    share = [[LZNetworkAvailability alloc] init];
}

-(instancetype)init{
    if (self = [super init]) {
            NSString *host = @"usersystem.test.com";
            SimplePing *pinger = [[SimplePing alloc] initWithHostName:host];
            pinger.delegate = self;
            pinger.addressStyle = SimplePingAddressStyleAny;
            self.pinger = pinger;
            [self.pinger start];
    }
    return self;
}

@end

接下来实现 SimplePing 的代理方法,SimplePingDelegate 一共有6个方法,分别对应 ping 的不同状态:

//开始进行网络检测
- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address;
//网络检测失败
- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error;
//发送网络包成功
- (void)simplePing:(SimplePing *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber;
//发送网络包失败
- (void)simplePing:(SimplePing *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error;
//收到网络包回应
- (void)simplePing:(SimplePing *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber;
//收到错误的网络包
- (void)simplePing:(SimplePing *)pinger didReceiveUnexpectedPacket:(NSData *)packet;

然后分别在开始进行网络检测和网络检测失败的代理方法中设置单利类 SingletonhasNet 属性的值,

/**
 *  start成功,也就是准备工作做完后的回调
 */
- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address
{
    // 发送测试报文数据
    [self.pinger sendPingWithData:nil];

    Singleton *sin = [Singleton shareInstance];
    if (!sin.hasNet) {
        sin.hasNet = YES;
        [[NSNotificationCenter defaultCenter] postNotificationName:networkCanBeUsedNotification object:nil];
    }
}

网络检测失败,

//网络检测失败
- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error
{
    NSLog(@"didFailWithError");
    [self.pinger stop];
    Singleton *sin = [Singleton shareInstance];
    sin.hasNet = YES;
    if (sin.isOverdue) {
        if ([[UIViewController getCurrentVC] isKindOfClass:[[LZMediator sharedInstance] LZLoginComponents_loginViewController].class] || [[UIViewController getCurrentVC] isKindOfClass:[[LZMediator sharedInstance] LZLoginComponents_forgetPwdViewController].class]) {
            if (sin.hasNet) {
                sin.hasNet = NO;
                [[NSNotificationCenter defaultCenter] postNotificationName:networkCanNotUsedNotification object:nil];
            }
            return;
        }
        LZCustomAlertView *alertView = [[LZCustomAlertView alloc] initWithTitle:@"温馨提示" content:@"很抱歉,过期诊所不可进入离线模式" close:nil certain:@"我知道了" closeButtonBlock:nil certainButtonBlock:^(LZCustomAlertView *alertView) {
            [LZMainHandler loginOutInitRemovePwd:NO backToLogin:YES];
            if (sin.hasNet) {
                sin.hasNet = NO;
                [[NSNotificationCenter defaultCenter] postNotificationName:networkCanNotUsedNotification object:nil];
            }
        }];
        [alertView show];
    }else{
        if (sin.hasNet) {
            sin.hasNet = NO;
            [[NSNotificationCenter defaultCenter] postNotificationName:networkCanNotUsedNotification object:nil];
        }
    }

}

2. 使用LZNetworkAvailability测试网络连接状态

基于 SimplePingLZNetworkAvailability 类可以在任何线程上使用,但是用作单例必须限制在指定的开启运行循环的线程。由于在应用程序启动时,就需要实现网络的检测,所有,在 AppDelegate 里进行初始化,

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //开始网络检测
    [LZNetworkAvailability shareInstanceNetworkAvailability];
}

注意:
这里需要特别注意的一点,网络请求会存在失败的情况,而网络连接问题同样会导致请求失败。这里就需要对这种情况失败情况进行检测,在封装的网络请求方法中,调用 shareInstanceNetworkAvailability 进行检测,

//基本请求接口,是否发送token失效通知
+ (void)baseRequestWithParam:(NSDictionary *)param
                        path:(NSString *)path
                      method:(LZHttpRequestType)method
                loseEfficacy:(BOOL)loseEfficacy
                     success:(void(^)(LZHttpResponseModel *responseModel))success
                     failure:(void(^)(NSError *error))failure{
    [[LZHttpClient defaultClient] requestWithPath:path method:method parameters:[self getDetailDicWithParam:param] prepareExecute:^{

    } success:^(AFHTTPRequestOperation *operation, id responseObject) {
        LZHttpResponseModel *model = [LZHttpResponseModel mj_objectWithKeyValues:responseObject];
        NSLog(@"\n****************response***********:\n%@\n *******************url************:\n%@ \n*****************param**********\n%@",responseObject,operation.request.URL,param);

        if ([model.body.code isKindOfClass:[NSString class]] && ![model.body.code isEqualToString:SUCCESS_CODE] && ![model.body.code isEqualToString:SUCCESS_CODE_NEW] && ![model.body.code isEqualToString:@"0330"] && ![model.body.code isEqualToString:@"0336"] && ![model.body.code isEqualToString:@"0798"]) {
        }
        if (success) {
            success(model);
        }
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        Class class = NSClassFromString(@"LZNetworkAvailability");
        BeginUndeclaredSelectorWarning
        [class performSelector:@selector(shareInstanceNetworkAvailability) withObject:nil];
        EndUndeclaredSelectorWarning
        if (failure) {
            failure(error);
        }
    }];
}

这里通过 NSClassFromString 找到动态加载的类,直接调用类中的方法。

3. 为确保您数据的准确性,请尽快恢复设备的网络连接!

离线数据的保存最长时间是多少?

离线登录需要判断离线时间,并给出提示?

离线数据在有网环境下的同步

利用 GCD 中的 dispatch_semaphore 即,信号量来实现不同模块数据的同步。

GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。Dispatch Semaphore 提供了三个函数。

离线数据上传,

#pragma mark - 离线数据上传
- (void)offLineAllDataUploadWithSync:(BOOL)sync {

    //每次上传都需要新创建一个数组来保存成功返回的流水号
    NSMutableArray *serialNumberMuArr = [[NSMutableArray alloc]init];
    [LZUserDefaults setObject:serialNumberMuArr forKey:PreferenceKey_OffLine_SerialNumberMuArr];

    //保存一个开关
    [LZUserDefaults setObject:@"0" forKey:PreferenceKey_OffLine_OfflineUploadComplete];

    //顺序执行 信号量
    dispatch_semaphore_t sem = dispatch_semaphore_create(1);
    dispatch_queue_t queue = dispatch_queue_create("OfflineDrugReductionInventoryoffLineAllDataUpload", NULL);
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        NSLog(@"WX患者模块");
        [self offLineNetWorkWithType:@"CP" withSem:sem];
//        dispatch_semaphore_signal(sem);
    });
    if (sync) {
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
            NSLog(@"WX设置模块");
            [self offLineNetWorkWithType:@"CE" withSem:sem];
            //        dispatch_semaphore_signal(sem);
        });
    }
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        NSLog(@"WX处方模块");
        [self offLineNetWorkWithType:@"RP" withSem:sem];
//        dispatch_semaphore_signal(sem);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        NSLog(@"WX零售模块");
        [self offLineNetWorkWithType:@"RS" withSem:sem];
//        dispatch_semaphore_signal(sem);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        NSLog(@"WX预约挂号");
        [self offLineNetWorkWithType:@"RE" withSem:sem];
        //        dispatch_semaphore_signal(sem);
    });
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        NSLog(@"WX数据上传完毕");
        [LZUserDefaults setObject:@"1" forKey:PreferenceKey_OffLine_OfflineUploadComplete];

        if (sync) {
            [self queryResultFromBackStage];
        }else {
            dispatch_async(dispatch_get_main_queue(), ^{
                self.actionBlock();
            });
        }

        dispatch_semaphore_signal(sem);
    });

}

数据同步遇到的一些问题

成功请求一些编辑信息的接口后,会调用相应模块的同步接口。(这样做的利与弊?是否适用于每个数据同步操作?)

后续的优化

现在生产上一半的问题是数据同步引起的,新的方案是引入第三方IM,诊所所有账户加入到聊天室,当数据产生并上传服务器成功后,发消息到聊天室,其他账户收到消息数据并处理。

写在最后

未完,待续……

参考资料

ping

iOS-ping 网络小工具

SimplePing

–EOF–

若无特别说明,本站文章均为原创,转载请保留链接,谢谢