[轉]Swift 基於 willSet & didSet 的訂閱block(Observable)

轉自:http://www.jianshu.com/p/f1acd9dcc384

原由:

MVC一直以來是代碼組織架構中蘋果公司所推崇的開發模式,但因爲工程類文件的日益增多,MVC中C層(Controller層)的日益臃腫,產品需求複雜化,迭代速度愈來愈快,老架構設計已經漸漸跟不上高強度的組件化團隊化開發了。最近一直在尋求一種開發模式,能讓多個團隊成員能夠同時開發且邏輯清晰。期間閱讀了不少文章,好比VIPER架構、UBer公司未開源的Riblets架構、MVVM架構等,最終決定本身針對MVVM進行一次架構改造,並加入VIPER的特色。其中MVVM的ViewModel的輕實現,當下被列爲攻堅環節。git

MVVM的ViewModel中採用KVO的觀察者模式監聽,調用ViewController來進行整個架構的解耦設計。在Objective-C當中得益於強大的Runtime機制能夠實現對任意類型的觀察者監聽。雖然Objective-C中能夠任意定義KVO,可是經歷過大項目的朋友必定首先會想到Objective-C中的KVO在使用的輕便型上差強人意,須要addObserver和removeObserver,且若是Context上下文弄錯了,會有必定的崩潰風險,這是須要深入瞭解Objective-C的釋放避免指針的循環引用等。github

Swift做爲一個靜態編譯型語言,它摒棄了Objective-C中的Runtime機制。想要開啓動態Property須要再Swift的Property前面增長聲明:dynamic,且使用dynamic必須是基於NSObject基類所構造的類型,這樣作必然會喪失對Swift原始數據類型的支持,可見其是很差的。並且預計沒有多少朋友記得給變量打上dynamic的標記吧,起碼我不會編程

很慶幸的是Swift語言在本身的Property中增長了getter/ setter的屬性觀察器,並對setter的屬性觀察器提供了willSet / didSet的兩個觀察器來詳細監聽值的變化。這讓咱們看到Swift自己是汲取了Objective-C在Runtime中創造的經驗和靈感,並將觀察者模式輕量化,以至關優雅的方式去表示一個值的變化過程。swift

class valueDemo { var value:String = "" { willSet { print("newValue:", newValue) } didSet { print("done:", value) } } }

但是咱們在開發中不只僅是這樣的簡單環境,咱們須要針對MVVM中ViewModel開放一個被觀察者鏈接給ViewController,二者產生聯動。此時有人想到:"我提供一個閉包(block)設置給didSet就行了呀"。確實你能夠這樣作。爲每個Property提供一個block雖然可行,但沒有重用好這一機制是則會讓代碼變得重複。那咱們就要尋找一個好一點的方法來能讓Property變成一個被觀察者,當它發生變動的時候,觸發一批block回調。api

分析第三方:

ReactiveCocoa和RxSwift的第三方庫來實現是能夠很好地實現觀察者模式(筆者更喜歡後者RxSwift的書寫風格)。確實,如今MVVM中採用RxSwift解耦做爲中間件確實是產品開發潮流,這就像某種服裝搭配趨勢同樣的流行。那問題隨之而來,採用ReactiveCocoa和RxSwift都哪些共同缺點呢?咱們開發實戰的時候確定會遇到下面的問題:數組

  1. 訂閱和分發致使它自己的執行效率低,會有大量的觸發棧和循環去進行訂閱消息的分發,遍歷逐個投遞。
  2. Swift自己的語法致使從Swift v.2 -> v.3 -> v.4的語法升級受制於蘋果的語言規則。Swift語言開發者的開發理念是快速激進式的開發(我給它定名爲:語法摧毀),雖然xcode提供了自動化轉換語法功能,但不免會有轉換錯誤和手動修改的狀況。這樣對於咱們程序自己是很是不穩定的變化,致使咱們出現重寫程序組件的問題,甚至摧毀式的沒法編譯
  3. ReactiveCocoa和RxSwift的開發成本比較高,語法體系「奇特」(碎片化的代碼,打散業務邏輯,由第三方庫限定語法編寫方式),致使團隊間在合做時邏輯代碼理解難度加大。團隊成員間的代碼溝通變慢。若是團隊加入新人,學習成本則會提升。
  4. 庫文件升級緩慢,受制於他人,若是中止更新,可能你的產品就要趕忙尋找其餘第三方庫來進行重構。

基於以上幾點缺點,我在這裏不贊同採用這樣的第三方組件的開發方式開發,雖然它們很酷炫、顯得高大上!xcode

全新建立:

那難道沒有一個又輕又容易維護的觀察者模式嗎?答案是有的!
那咱們就從零開始一步步實現一個基於Swift 3~4的低調奢華有內涵的觀察者模式(題外話因爲我所書寫的日期是2017-6-6,正好是Swift 4發佈當日,個人工程文件又一次被Swift4的升級所摧毀,被摧毀的是第三方庫,那我仍是本身造一個輪子吧!)閉包

先來描述一下基本原理:架構

  1. 實現一個用於產生被觀察者的自定義泛型類:Observable<T>
  2. Observable自身提供blocks的閉包數組存放訂閱者的閉包
  3. 基於Observable中的value的setter方法,手動調用每一個閉包

先來看一下基礎代碼:app

// 須要持有一批blocks,則必須建立一個類做爲空間 class Observable<T> { typealias ObservableBlock = (T) -> () private var blocks: [ObserverBlock] = [] // 持有blocks init(_ t:T) { self.value = t } // 初始化value var value:T { didSet { // 實現didSet來遍歷block,觸發回調 for block in blocks { block(self.value) } } } // 訂閱 func subscribe(block:@escaping ObserverBlock) { blocks.append(block) } }

run exmple:

let example = Observable<String>("") example.subscribe { (newValue:String) in print("newValue:", newValue) } example.value = "a" example.value = "b"

代碼的運行結果:

newValue: a newValue: b

看到運行結果,很不錯!基於簡單blocks持有,基於didSet就能夠完成對於一個變量設置的變動監聽。

繼續完善

仔細打量了代碼,中間缺乏幾個能力:

  1. 如何將example.value = "a"的寫法,將開發者的敲擊鍵盤所消耗的卡路里降到最低呢。賦值形式換爲:example <- "a"
    這裏想到了Swift的《高級運算符重載》:【https://www.cnswift.org/advanced-operators#spl-17】
  2. 缺乏刪除訂閱者block能力。這個能力須要在訂閱時將訂閱者傳遞給Observable加以持有,並提供unSubscribe方法

第一步咱們先來加入高級運算符重載,片斷代碼:

infix operator <-: ObservableChange precedencegroup ObservableChange { associativity: left // 表示左結合 } public func <- <T> (left: Observable<T>, right: T) { left.value = right }

完整代碼:[純block,可自動釋放內存]

// 高級運算符重載必須聲明在final頂級訪問級別的類中 public final class Observable<T> { typealias ObserverBlock = (T) -> () private var blocks: Array<ObserverBlock> = Array() init(_ t:T) { self.value = t } var value:T { didSet { for block in blocks { block(self.value) } } } func subscribe(block:@escaping ObserverBlock) { blocks.append(block) } deinit { print("Observable", #function) } } /* 定義 <- 運算符 運算符定義必須放在文件級別當中 */ infix operator <-: ObservableChange precedencegroup ObservableChange { associativity: left // 表示左結合 } public func <- <T> (left: Observable<T>, right: T) { left.value = right }

run exmple :

let example = Observable<String>("") example.subscribe { (newValue:String) in print("newValue:", newValue) } example.value = "a" example.value = "b" example <- "a"

代碼的運行結果:

newValue: a newValue: b newValue: a

重載看上去還不錯,很精簡!那繼續完善,填補後續的功能

第二步添加unSubscribe方法

起初我想直接經過block閉包的相等性檢查,經過block閉包相等,來移除blocks中的指定閉包,可是失敗了。好比代碼:

public final class Observable<T> { typealias ObserverBlock = (T) -> () private var blocks: Array<ObserverBlock> = Array() init(_ t:T) { self.value = t } var value:T { didSet { for block in blocks { block(self.value) } } } func subscribe(block:@escaping ObserverBlock) { blocks.append(block) } // 移除訂閱 func unSubscript(block:@escaping ObservableBlock) { var blocksFiltered = blocks.filter { (blockInArray:ObservableBlock) -> Bool in return blockInArray !== block // !!!!!!!沒法編譯,編譯報錯!!!!!!! //報錯信息: Cannot check reference equality of functions;operands here have type '(T)->()' and '(T)->()' } self.blocks = blocksFiltered } }

看到//報錯信息: Cannot check reference equality of functions;operands here have type '(T)->()' and '(T)->()'
發現Swift中是不容許將兩個閉包進行的比較的。雖然遺留的C API中是有unsafeBitCast能夠對兩個閉包進行比較,但我仍是放棄這樣的寫法。

unsafeBitCast 相關使用:https://stackoverflow.com/questions/24111984/how-do-you-test-functions-and-closures-for-equality

那既然block沒法比較相等,就只能講上下文與blocks進行綁定關係,來實現訂閱和刪除訂閱。

// 定義高級運算符重載,必須爲final訪問權限的聲明 public final class Observable<T> { typealias ObserverBlock = (_ oldValue:T, _ newValue:T) -> () // 訂閱block,增長old和new的傳值 typealias ObserverEntry = (observer: AnyObject, block: ObserverBlock) // 觀察者元組 private var observers: [ObserverEntry] // 觀察者Array init(_ value:T) { self.value = value observers = [] } var value:T { didSet { observers.forEach { (entry: ObserverEntry) in let (_, block) = entry block(oldValue, value) } } } // 訂閱,建立觀察者元組 func subscribe(observer:AnyObject, block:@escaping ObserverBlock) { observers.append(ObserverEntry(observer:observer, block:block)) } // 解除訂閱,根據元組中的觀察者移除 func unSubscribe(observer:AnyObject) { let filtered = observers.filter { (entry: ObserverEntry) in let (owner, _) = entry return owner !== observer } observers = filtered } } infix operator <-: ObservableChange precedencegroup ObservableChange { associativity: left // 表示左結合 } // 運算符重載 public func <- <T> (left: Observable<T>, right: T) { left.value = right }

run example:

let example = Observable<String>("") example.subscribe(observer: self) { (oldValue:String, newValue:String) in print("oldValue:", oldValue, "newValue:", newValue) } example.value = "a" example.value = "b" example <- "a" example.unSubscribe(observer: self) example <- "c" // 取消訂閱,則不會看到"c"的打印

代碼的運行結果:

oldValue:  newValue: a oldValue: a newValue: b oldValue: b newValue: a // 這裏沒有看到「c」

好了,通過細細打磨的Observable已經初步具有了觀察者能力了,而且能夠輕巧的應用於變量的觀察
所有代碼:
https://github.com/slazyk/Observable-Swift

我在編寫期間試用了google的一個開發者開發的Observable-Swift的,但這個只針對於Swift 3且功能略顯複雜,最後放棄。

已知弊端:

不過本觀察者訂閱模式和其餘的第三方組件其實都有弊端:

  1. 就是插入式編程,
  2. 內存循環應用
    插入式編程就是會將原有的代碼的變量類型破壞,從而讓類型都趨向於Observable<T>數據類型,這樣喜歡純正變量監聽的話,當下除了willSet和didSet,還沒有發現其餘更優雅的方法!

而內存循環應用,須要將被保存在entry當中的Observer在必要的時候unSubscribe掉才能夠解決循環引用的問題。因此當以爲Observer沒有必要的話,仍是直接使用只有一個Block的版本吧![純block,可自動釋放內存]

相關文章
相關標籤/搜索