很多时候产品们都有一些奇奇怪怪的想法和要求,这里我们就有一个需求,要求我们应用里面所有的用户行为数,比如阅读数、点赞数、评论数和关注、点赞状态等全局同步,一旦有变更要求全局更新显示。
准备
开始我们考虑了一种方案,创建一个池子,所有同一类型的Model都存放在池子里面,使用时优先在池子里面取,不存在时创建并加入池子。这样我们就能够确保我们应用里面的所有“同一对象”,是真正的同一个对象。
但是这样做也存在很多问题:
- 当这个需求提出来开始做的时候我们的应用已经基本成型,很多接口和model并没有统一,如果要采用这种方案必然需要大改。
- 这样做势必会导致model的冗余属性。
- 接口有些时候放回相同字段,但是意义不一致。
- 第三方库的支持。比如YYModel的解析需要修改很多地方才能使用。
所以考虑了以下的方案。
方案
思路保持一致,将需要同步的对象加入全局的池子。但是各自创建各自的对象,在需要全局同步的时候,提交该对应的keyPath,然后更新池子中拥有相同类
的成员。在view层,使用KVO监听变化。
缺点:
由于根据了类名来作为判断该对象是否属于同一对象,所以继承或者拥有不同类名的“同一对象”并不能被识别为相同的。
在我们已经比较完善的项目中,要做这样的统一,几乎是不可能的,所以特例化了部分场景,来满足我们当前的需求。
方案优化版
我分析了我们应用中需要使用到全局同步的对象,可以分为几种类型(比如动态、评论等),并不会存在特别复杂的类型。而且每种类型必定会存在一个唯一的ID,所以觉得可以通过type和ID来唯一确定是“同一个对象”。
所以将结构修改为下,所有需要支持全局同步的类都需要实现下面的协议。
1 | @protocol MZChannelProtocol <NSObject> |
接口设计如下
1 | @interface MZChannel : NSObject |
同时在使用KeyPath的过程中需要判断是否合法,防止某些对象不存在该成员而crash。
1 | // 这里使用set方法来判断是否可以同步,所以实际上只要实现了对应的set方法就可以了,并不需要实际的property。 |
池子的实现,把整个池子分为若干桶,每个桶的key为相应的type,桶使用weak类型的hashTable来实现存储。
这里需要注意的是一些多线程可能导致的问题,所以在更新操作中使用了锁。由于我们应用内“同一对象”和“同类型对象”的数目预估应该存在不超过1000个,所以不需要考虑性能问题,也就可以在主线程中同步数据。
1 | @interface MZChannelObject : NSObject |
使用
1 | @interface MZUser : NSObject <MZChannelProtocol> |
在请求关注或者取消关注的时候触发同步
1 | [user emitKeyPath:NSStringFromSelector(@selector(followed)) forValue:@(YES)]; |
然后使用KVO来观察对象变化
1 | [self.KVOController observe:_user keyPath:NSStringFromSelector(@selector(followed)) options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, MZUser *object, NSDictionary<NSString *,id> * _Nonnull change) { |
缺点
虽然实现了全局同步,但是由于使用了统一的池子,会导致DEBUG困难。
需要实现人工判断更新的内容。
KVO不能判断该更新是用户操作引起的,还是由其他对象变更引起的。这里可能涉及到行为动画,但是我们的业务场景不可能一个页面出现两个相同的内容,所以并没有什么影响。
虽然可以使用KVO来实现同步UI的更新,但并没有做到和MVVM一样的同步更新,还是需要人工处理更新逻辑。
有一定的代码侵入性,需要继承协议,并且在初始化的时候加入池子。
总结
这里限制了一部分的使用场景,来满足了特定环境下的需求,希望能给其他需要同步数据的场景一个方法。