美文网首页
客户端全局数据同步方案(一)

客户端全局数据同步方案(一)

作者: 码农苍耳 | 来源:发表于2017-04-09 18:15 被阅读449次

    很多时候产品们都有一些奇奇怪怪的想法和要求,这里我们就有一个需求,要求我们应用里面所有的用户行为数,比如阅读数、点赞数、评论数和关注、点赞状态等全局同步,一旦有变更要求全局更新显示。

    准备

    开始我们考虑了一种方案,创建一个池子,所有同一类型的Model都存放在池子里面,使用时优先在池子里面取,不存在时创建并加入池子。这样我们就能够确保我们应用里面的所有“同一对象”,是真正的同一个对象。

    但是这样做也存在很多问题:

    1. 当这个需求提出来开始做的时候我们的应用已经基本成型,很多接口和model并没有统一,如果要采用这种方案必然需要大改。
    2. 这样做势必会导致model的冗余属性。
    3. 接口有些时候放回相同字段,但是意义不一致。
    4. 第三方库的支持。比如YYModel的解析需要修改很多地方才能使用。

    所以考虑了以下的方案。

    方案

    思路保持一致,将需要同步的对象加入全局的池子。但是各自创建各自的对象,在需要全局同步的时候,提交该对应的keyPath,然后更新池子中拥有相同类的成员。在view层,使用KVO监听变化。

    缺点:

    由于根据了类名来作为判断该对象是否属于同一对象,所以继承或者拥有不同类名的“同一对象”并不能被识别为相同的。

    在我们已经比较完善的项目中,要做这样的统一,几乎是不可能的,所以特例化了部分场景,来满足我们当前的需求。

    方案优化版

    structure.png

    我分析了我们应用中需要使用到全局同步的对象,可以分为几种类型(比如动态、评论等),并不会存在特别复杂的类型。而且每种类型必定会存在一个唯一的ID,所以觉得可以通过type和ID来唯一确定是“同一个对象”。

    所以将结构修改为下,所有需要支持全局同步的类都需要实现下面的协议。

    @protocol MZChannelProtocol <NSObject>
    
    @property (readonly, nonatomic) NSString *id;
    @property (readonly, nonatomic) NSInteger channelType;
    
    @optional
    // 提供一个keyPath转换的方法
    - (NSString *)translateKeyPath:(NSString *)keyPath;
    
    @end
    

    接口设计如下

    @interface MZChannel : NSObject
    
    + (instancetype)sharedChannel;
    
    // 需要在类创建完之后加入池子,一般在init方法中
    - (void)addObject:(id<MZChannelProtocol>)obj;
    
    - (void)emitType:(NSInteger)type id:(NSString *)id keyPath:(NSString *)keyPath forValue:(id)value;
    
    @end
    

    同时在使用KeyPath的过程中需要判断是否合法,防止某些对象不存在该成员而crash。

    // 这里使用set方法来判断是否可以同步,所以实际上只要实现了对应的set方法就可以了,并不需要实际的property。
    - (BOOL)canPerformKeyPath:(NSString *)keyPath newKeyPath:(out NSString **)aKeyPath {
        if ([self conformsToProtocol:@protocol(MZChannelProtocol)] && keyPath.length > 0) {
            id<MZChannelProtocol> cself = (id<MZChannelProtocol>)self;
            if ([cself channelType] <= 0) {
                return NO;
            }
            NSString *selectorStr = [NSString stringWithFormat:@"set%@%@:", keyPath.firstLetter.uppercaseString, [keyPath substringFromIndex:1]];
            if ([self respondsToSelector:NSSelectorFromString(selectorStr)]) {
                return YES;
            }
            else if ([cself respondsToSelector:@selector(translateKeyPath:)]) {
                NSString *transKeyPath = [cself translateKeyPath:keyPath];
                if (transKeyPath) {
                    if (transKeyPath.length > 0) {
                        selectorStr = [NSString stringWithFormat:@"set%@%@:", transKeyPath.firstLetter.uppercaseString, [transKeyPath substringFromIndex:1]];
                        if ([self respondsToSelector:NSSelectorFromString(selectorStr)]) {
                            if (aKeyPath) *aKeyPath = selectorStr;
                            return YES;
                        }
                    }
                }
            }
        }
        return NO;
    }
    

    池子的实现,把整个池子分为若干桶,每个桶的key为相应的type,桶使用weak类型的hashTable来实现存储。

    这里需要注意的是一些多线程可能导致的问题,所以在更新操作中使用了锁。由于我们应用内“同一对象”和“同类型对象”的数目预估应该存在不超过1000个,所以不需要考虑性能问题,也就可以在主线程中同步数据。

    @interface MZChannelObject : NSObject
    @property (assign, nonatomic) NSInteger type;
    @property (strong, nonatomic) NSHashTable<id<MZChannelProtocol>> *hashTable;
    @property (strong, nonatomic) NSLock *lock;
    
    - (void)addObject:(id<MZChannelProtocol>)object;
    - (void)emitType:(NSInteger)type id:(NSString *)id keyPath:(NSString *)keyPath forValue:(id)value;
    @end
    
    @implementation MZChannelObject
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            _hashTable = [[NSHashTable alloc] initWithOptions:NSPointerFunctionsWeakMemory capacity:0];
            _lock = [[NSLock alloc] init];
        }
        return self;
    }
    
    - (void)addObject:(id<MZChannelProtocol>)object {
        [self.lock lock];
        [_hashTable addObject:object];
        [self.lock unlock];
    }
    
    - (void)emitType:(NSInteger)type id:(NSString *)id keyPath:(NSString *)keyPath forValue:(id)value {
        if (type == self.type) {
            [self.lock lock];
            for (NSObject<MZChannelProtocol> *obj in _hashTable) {
                NSString *aKeyPath = nil;
                if ([obj.id isEqualToString:id] && [obj canPerformKeyPath:keyPath newKeyPath:&aKeyPath]) {
                    dispatch_block_t updateValue =^() {
                        if (aKeyPath) {
                            [obj setValue:value forKey:aKeyPath];
                        }
                        else {
                            [obj setValue:value forKey:keyPath];
                        }
                    };
                    if ([NSThread currentThread].isMainThread) {
                        updateValue();
                    }
                    else {
                        // 防止KVO刷新页面的时候的子线程操作UI
                        dispatch_sync(dispatch_get_main_queue(), updateValue);
                    }
                }
            }
            [self.lock unlock];
        }
    }
    
    @end
    

    使用

    @interface MZUser : NSObject <MZChannelProtocol>
    @property (strong, nonatomic) NSString *id;
    @end
    
    @implementation MZUser
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            [[MZChannel sharedChannel] addObject:self];
        }
        return self;
    }
    
    - (NSInteger)channelType {
        return MZResourceTypeUser;
    }
    @end
    

    在请求关注或者取消关注的时候触发同步

    [user emitKeyPath:NSStringFromSelector(@selector(followed)) forValue:@(YES)];
    或者
    [[MZChannel sharedChannel] emitType:MZResourceTypeUser id:user.id keyPath:NSStringFromSelector(@selector(followed)) forValue:@(YES)];
    

    然后使用KVO来观察对象变化

    [self.KVOController observe:_user keyPath:NSStringFromSelector(@selector(followed)) options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, MZUser *object, NSDictionary<NSString *,id> * _Nonnull change) {
                // update UI ...
            }];
    

    缺点

    虽然实现了全局同步,但是由于使用了统一的池子,会导致DEBUG困难。

    需要实现人工判断更新的内容。

    KVO不能判断该更新是用户操作引起的,还是由其他对象变更引起的。这里可能涉及到行为动画,但是我们的业务场景不可能一个页面出现两个相同的内容,所以并没有什么影响。

    虽然可以使用KVO来实现同步UI的更新,但并没有做到和MVVM一样的同步更新,还是需要人工处理更新逻辑。

    有一定的代码侵入性,需要继承协议,并且在初始化的时候加入池子。

    总结

    这里限制了一部分的使用场景,来满足了特定环境下的需求,希望能给其他需要同步数据的场景一个方法。

    相关文章

      网友评论

          本文标题:客户端全局数据同步方案(一)

          本文链接:https://www.haomeiwen.com/subject/ehadattx.html