数据不可变之linked-in/rocketdata

背景

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

book.islike = yes

这样会存在什么问题呢?git

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

怎么解决这种问题?编程

结果方案有以下几种网络

  1. 加锁:atomic 属性,可是这样毫无疑问会严重印象到 app 的总体效率,毕竟全部的读取都是在锁的环境下进行了。多线程

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

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

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

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

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

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 很是好的一点是它包装了全部的通知以及更新过程,你不须要手动的去注册各类同志,而且不用担忧通知遗漏。不过使用这套东西,对于编程习惯也是一种不小的挑战,要想真正运用到本身的项目,还有有必定挑战的。

相关文章
相关标签/搜索