很多时候产品们都有一些奇奇怪怪的想法和要求,这里我们就有一个需求,要求我们应用里面所有的用户行为数,比如阅读数、点赞数、评论数和关注、点赞状态等全局同步,一旦有变更要求全局更新显示。
准备
开始我们考虑了一种方案,创建一个池子,所有同一类型的Model都存放在池子里面,使用时优先在池子里面取,不存在时创建并加入池子。这样我们就能够确保我们应用里面的所有“同一对象”,是真正的同一个对象。
但是这样做也存在很多问题:
- 当这个需求提出来开始做的时候我们的应用已经基本成型,很多接口和model并没有统一,如果要采用这种方案必然需要大改。
- 这样做势必会导致model的冗余属性。
- 接口有些时候放回相同字段,但是意义不一致。
- 第三方库的支持。比如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一样的同步更新,还是需要人工处理更新逻辑。
有一定的代码侵入性,需要继承协议,并且在初始化的时候加入池子。
总结
这里限制了一部分的使用场景,来满足了特定环境下的需求,希望能给其他需要同步数据的场景一个方法。
网友评论