資料不可變之linked-in/rocketdata

zachwang發表於2017-01-07

背景

在我們通常的資料可變的資料框架中,我們從 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 非常好的一點是它包裝了所有的通知以及更新過程,你不需要手動的去註冊各種同志,並且不用擔心通知遺漏。不過使用這套東西,對於程式設計習慣也是一種不小的挑戰,要想真正運用到自己的專案,還有有一定挑戰的。

相關文章