数据不可变之linked-in/rocketdata

背景

在我们通常的数据可变的数据框架中,我们从 db 读取的数据放在 cache 里面供上层业务调用。比如一个 book 对象,如果我们在上层业务中有多个地方都需要用到这个 book 对象,那么其实我们是直接在引用这个对象。如果我们需要修改 book 对象的 islike 属性,我们会直接调用进行修改

book.islike = yes

这样会存在什么问题呢?

试想一下多线程的情况,我们在 thread1 在读取 book 对象的 islike 属性,thread2 在修改 book 的 islike 属性,这两个操作同时发生,这个时候就会导致 crash。

怎么解决这种问题?

结果方案有如下几种

  1. 加锁:atomic 属性,但是这样毫无疑问会严重印象到 app 的整体效率,毕竟所有的读取都是在锁的环境下进行了。

  2. 对象实行线程隔离。比如 realm ,在每一个线程中,它都保有一份被引用对象的线程快照,不同线程间的数据是独立的,同时读写并不会造成多线程的问题。

  3. 数据不可变,比如本文将要介绍的 rocketdata ,多线程的问题出现在对同一个对象同时写,或同时读写。数据不可变要求所有从 db 读出来的对象不允许在上层对它直接进行修改,只能读取,当你要修改一个对象的时候,你需要生成一个全新的对象,对这个全新对象进行修改,然后替换掉所有用到的旧对象,这样就能杜绝同时读写以及同时写的操作了。

下面讲重点分析不可变解决方案 rocketdata 的实现。

不可变对象需要面临的问题

因为我们从 db 读出来的数据,有可能被上层直接指针引用,也可能被拷贝出去,这些对象分散在不同的场景,因此不可变对象的使用中,一个很大的问题就在于当一个对象被改变的时候,如何去更新整个 app 中所有持有该对象的旧对象。

rocketdata的方案

rocketdata 为了实现对象的更新,它对所有的业务层对象都进行了一层包装,提供了 DataProvider 对单个对象的封装, CollectionDataProvider 对列表数据的封装,真实的数据存在于这些 provider 里面。

当我们设置 datasource 的时候,provider 不仅会保存当前数据,还会监听当前数据源里面所有的数据,包括这些数据的子数据。比如我们有一个 books 列表,每一个 book 里面都有一个 author 对象,那么在设置这个 books 列表的时候,provider除了将 books 保留之外,还会监听每一个 book 对象以及每一个 book 对象里面的 author,并且通知所有监听这些对象的其他 provider 去更新相应的数据。

以CollectionDataProvider为例,当我们网络请求回来数据的时候,我们调用如下接口更新当前 datasource

 NetworkManager.fetchChats { (models, error) in
            if error == nil {
                self.dataProvider.setData(models, cacheKey: self.cacheKey)
                self.tableView.reloadData()
            }
        }

setData 做了什么呢?下面是它的简化代码

 open func setData(_ data: [T], cacheKey: String?, shouldCache: Bool = true, context: Any? = nil) {
 
        self.dataHolder.setData(data, changeTime: ChangeTime())
 ....       updateAndListenToNewModelsInConsistencyManager(context: context)
 }

它做了两件事,一个是更新当前CollectionDataProvider的数据,另外一个就是更新其他监听了这些数据的provider,同时监听新的对象。

需要说明,它这个监听并不是我们普通意义上的 addobserver 或者 kvc , rocketdata 建议所有的 provider 都持有一个 datamodlemanager 单例,datamodlemanager 包含了一个 consistencyManager ,consistencyManager 负责同步并持有一个 listeners 字典,里面纪录了所有的监听。

监听做的事情,就是为每一个 modelIdentifier 记录一个列表,这个列表保存了所有监听的 provider(这个 modelIdentifier 对于 model 而言相当于主键,每个 provider 也会有一个自己的modelIdentifier,用于区分不同的对象),当一个特定modelIdentifier 的 model 更新的时候,根据listener[modelIdentifier] 找到所有的监听者,并进行更新,简单代码如下。

 private func addListener(_ listener: ConsistencyManagerListener, recursivelyToChildModels model: ConsistencyManagerModel) {
        // Add this model as listening
        addListener(listener, toModel: model)
        // Recurse on the children
        model.forEach() { child in
            self.addListener(listener, recursivelyToChildModels: child)
        }
    }
private func addListener(_ listener: ConsistencyManagerListener, toModel model: ConsistencyManagerModel) {
        let id = model.modelIdentifier
        if let id = id {
            var weakArray: WeakListenerArray = {
               ...
            }()
            let alreadyInArray: Bool = {
                ...
            }()
            if !alreadyInArray {
                weakArray.append(listener)
                listeners[id] = weakArray
            }
        }
    }

当我们更新数据的时候,我们就拿这些改变的数据去找所有监听的 provider

open func updateModel(_ model: ConsistencyManagerModel, context: Any? = nil) {
        dispatchTask { cancelled in
            let tuple = self.childrenAndListenersForModel(model)
            ...  
            self.updateListeners(tuple.listeners, withUpdatedModels: optionalModelUpdates, context: context, cancelled: cancelled)
        }
    }

这里的 childrenAndListenersForModel 就是找到当前变更对象的子对象以及 consistencyManager 里面的监听列表,代码如下:

 private func childrenAndListenersForModel(_ model: ConsistencyManagerModel, modelUpdates: DictionaryHolder<String, [ConsistencyManagerModel]>, listenersArray: ArrayHolder<ConsistencyManagerListener>) {

        if let id = model.modelIdentifier {
            //modified model
            modelUpdates.dictionary[id] = projections
            
            //listeners to id (need avoid repeat listener)
            listenersArray.array.append(listeners[id])  
                }

        model.forEach { child in
            self.childrenAndListenersForModel(child, modelUpdates: modelUpdates, listenersArray: listenersArray)
        }
    }

找到当前变化的 models 以及相关的所有 listeners 后,就可以开始真正的更新过程。

 private func updateListeners(_ listeners: [ConsistencyManagerListener], withUpdatedModels updatedModels: [String: [ConsistencyManagerModel]?], context: Any?, cancelled: ()->Bool) {
  For each listener:
     1. Gets the current model from listener
     2. Generates the new model.
     3. Generates the list of changed and deleted ids.
     4. Ensures that listener listens to all the new models that have been added.
     5. Notifies the listener of the new model change.

 }

上面做的事情就是遍历所有的待更新的 listeners ,并对 listener 持有的数据( currentModel )与更新的数据进行比较,看其中的数据是否发生了变化,如果有变化,则进行替换。这个替换是以listener 为粒度进行的,也就是如果你更新多个 models ,然后这多个 models 和某个 provider 关联,那么其实这些 mdels 的更新是一次性进行的。更新代码如下

open func modelUpdated(_ model: ConsistencyManagerModel?, updates: ModelUpdates, context: Any?) {
...
 dataHolder.setData(newData, changeTime: changeTime ?? ChangeTime())
 ...
}

上面的方法是在 provider 里面执行的,它利用 updates 生成新的 newdata ,然后替换掉当前 provider 所持有的 data 数据。

更改完成后,通过回调通知相应的controller,刷新界面

 func collectionDataProviderHasUpdatedData<T>(_ dataProvider: CollectionDataProvider<T>, collectionChanges: CollectionChange, context: Any?) {
        self.tableView.reloadData()
}

结束语

rocketdata 非常好的一点是它包装了所有的通知以及更新过程,你不需要手动的去注册各种同志,并且不用担心通知遗漏。不过使用这套东西,对于编程习惯也是一种不小的挑战,要想真正运用到自己的项目,还有有一定挑战的。

    原文作者:zachwang
    原文地址: https://segmentfault.com/a/1190000008043161
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞