客戶端全局?jǐn)?shù)據(jù)同步方案(一)

很多時(shí)候產(chǎn)品們都有一些奇奇怪怪的想法和要求,這里我們就有一個(gè)需求,要求我們應(yīng)用里面所有的用戶行為數(shù),比如閱讀數(shù)、點(diǎn)贊數(shù)、評(píng)論數(shù)和關(guān)注、點(diǎn)贊狀態(tài)等全局同步,一旦有變更要求全局更新顯示。

準(zhǔn)備

開始我們考慮了一種方案,創(chuàng)建一個(gè)池子,所有同一類型的Model都存放在池子里面,使用時(shí)優(yōu)先在池子里面取,不存在時(shí)創(chuàng)建并加入池子。這樣我們就能夠確保我們應(yīng)用里面的所有“同一對(duì)象”,是真正的同一個(gè)對(duì)象。

但是這樣做也存在很多問(wèn)題:

  1. 當(dāng)這個(gè)需求提出來(lái)開始做的時(shí)候我們的應(yīng)用已經(jīng)基本成型,很多接口和model并沒有統(tǒng)一,如果要采用這種方案必然需要大改。
  2. 這樣做勢(shì)必會(huì)導(dǎo)致model的冗余屬性。
  3. 接口有些時(shí)候放回相同字段,但是意義不一致。
  4. 第三方庫(kù)的支持。比如YYModel的解析需要修改很多地方才能使用。

所以考慮了以下的方案。

方案

思路保持一致,將需要同步的對(duì)象加入全局的池子。但是各自創(chuàng)建各自的對(duì)象,在需要全局同步的時(shí)候,提交該對(duì)應(yīng)的keyPath,然后更新池子中擁有相同類的成員。在view層,使用KVO監(jiān)聽變化。

缺點(diǎn):

由于根據(jù)了類名來(lái)作為判斷該對(duì)象是否屬于同一對(duì)象,所以繼承或者擁有不同類名的“同一對(duì)象”并不能被識(shí)別為相同的。

在我們已經(jīng)比較完善的項(xiàng)目中,要做這樣的統(tǒng)一,幾乎是不可能的,所以特例化了部分場(chǎng)景,來(lái)滿足我們當(dāng)前的需求。

方案優(yōu)化版

structure.png

我分析了我們應(yīng)用中需要使用到全局同步的對(duì)象,可以分為幾種類型(比如動(dòng)態(tài)、評(píng)論等),并不會(huì)存在特別復(fù)雜的類型。而且每種類型必定會(huì)存在一個(gè)唯一的ID,所以覺得可以通過(guò)type和ID來(lái)唯一確定是“同一個(gè)對(duì)象”。

所以將結(jié)構(gòu)修改為下,所有需要支持全局同步的類都需要實(shí)現(xiàn)下面的協(xié)議。

@protocol MZChannelProtocol <NSObject>

@property (readonly, nonatomic) NSString *id;
@property (readonly, nonatomic) NSInteger channelType;

@optional
// 提供一個(gè)keyPath轉(zhuǎn)換的方法
- (NSString *)translateKeyPath:(NSString *)keyPath;

@end

接口設(shè)計(jì)如下

@interface MZChannel : NSObject

+ (instancetype)sharedChannel;

// 需要在類創(chuàng)建完之后加入池子,一般在init方法中
- (void)addObject:(id<MZChannelProtocol>)obj;

- (void)emitType:(NSInteger)type id:(NSString *)id keyPath:(NSString *)keyPath forValue:(id)value;

@end

同時(shí)在使用KeyPath的過(guò)程中需要判斷是否合法,防止某些對(duì)象不存在該成員而crash。

// 這里使用set方法來(lái)判斷是否可以同步,所以實(shí)際上只要實(shí)現(xiàn)了對(duì)應(yīng)的set方法就可以了,并不需要實(shí)際的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;
}

池子的實(shí)現(xiàn),把整個(gè)池子分為若干桶,每個(gè)桶的key為相應(yīng)的type,桶使用weak類型的hashTable來(lái)實(shí)現(xiàn)存儲(chǔ)。

這里需要注意的是一些多線程可能導(dǎo)致的問(wèn)題,所以在更新操作中使用了鎖。由于我們應(yīng)用內(nèi)“同一對(duì)象”和“同類型對(duì)象”的數(shù)目預(yù)估應(yīng)該存在不超過(guò)1000個(gè),所以不需要考慮性能問(wèn)題,也就可以在主線程中同步數(shù)據(jù)。

@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刷新頁(yè)面的時(shí)候的子線程操作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

在請(qǐng)求關(guān)注或者取消關(guān)注的時(shí)候觸發(fā)同步

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

然后使用KVO來(lái)觀察對(duì)象變化

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

缺點(diǎn)

雖然實(shí)現(xiàn)了全局同步,但是由于使用了統(tǒng)一的池子,會(huì)導(dǎo)致DEBUG困難。

需要實(shí)現(xiàn)人工判斷更新的內(nèi)容。

KVO不能判斷該更新是用戶操作引起的,還是由其他對(duì)象變更引起的。這里可能涉及到行為動(dòng)畫,但是我們的業(yè)務(wù)場(chǎng)景不可能一個(gè)頁(yè)面出現(xiàn)兩個(gè)相同的內(nèi)容,所以并沒有什么影響。

雖然可以使用KVO來(lái)實(shí)現(xiàn)同步UI的更新,但并沒有做到和MVVM一樣的同步更新,還是需要人工處理更新邏輯。

有一定的代碼侵入性,需要繼承協(xié)議,并且在初始化的時(shí)候加入池子。

總結(jié)

這里限制了一部分的使用場(chǎng)景,來(lái)滿足了特定環(huán)境下的需求,希望能給其他需要同步數(shù)據(jù)的場(chǎng)景一個(gè)方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來(lái)就是把...
    Dove_iOS閱讀 27,217評(píng)論 30 472
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,552評(píng)論 25 708
  • 面試題參考1 : 面試題[http://www.cocoachina.com/ios/20150803/12872...
    江河_ios閱讀 1,759評(píng)論 0 4
  • 序言 目前形勢(shì),參加到iOS隊(duì)伍的人是越來(lái)越多,甚至已經(jīng)到供過(guò)于求了。今年,找過(guò)工作人可能會(huì)更深刻地體會(huì)到今年的就...
    Jack_lin閱讀 78,577評(píng)論 110 1,945
  • 第一天完美 第二天頹廢 厭
    EmmaBU閱讀 163評(píng)論 0 0