Swift 基于 willSet & didSet 的訂閱block(Observable)

本文原創(chuàng) 作者:叢瑜帥 如需轉(zhuǎn)載請(qǐng)注明 (2017-06-06)

起因:

MVC一直以來是代碼組織架構(gòu)中蘋果公司所推崇的開發(fā)模式,但由于工程類文件的日益增多,MVC中C層(Controller層)的日益臃腫,產(chǎn)品需求復(fù)雜化,迭代速度越來越快,老架構(gòu)設(shè)計(jì)已經(jīng)漸漸跟不上高強(qiáng)度的組件化團(tuán)隊(duì)化開發(fā)了。最近一直在尋求一種開發(fā)模式,能讓多個(gè)團(tuán)隊(duì)成員可以同時(shí)開發(fā)且邏輯清晰。期間閱讀了很多文章,比如VIPER架構(gòu)、UBer公司未開源的Riblets架構(gòu)、MVVM架構(gòu)等,最終決定自己針對(duì)MVVM進(jìn)行一次架構(gòu)改造,并加入VIPER的特點(diǎn)。其中MVVM的ViewModel的輕實(shí)現(xiàn),當(dāng)下被列為攻堅(jiān)環(huán)節(jié)。

MVVM的ViewModel中采用KVO的觀察者模式監(jiān)聽,調(diào)用ViewController來進(jìn)行整個(gè)架構(gòu)的解耦設(shè)計(jì)。在Objective-C當(dāng)中得益于強(qiáng)大的Runtime機(jī)制可以實(shí)現(xiàn)對(duì)任意類型的觀察者監(jiān)聽。雖然Objective-C中可以任意定義KVO,但是經(jīng)歷過大項(xiàng)目的朋友一定首先會(huì)想到Objective-C中的KVO在使用的輕便型上差強(qiáng)人意,需要addObserver和removeObserver,且如果Context上下文弄錯(cuò)了,會(huì)有一定的崩潰風(fēng)險(xiǎn),這是需要深刻了解Objective-C的釋放避免指針的循環(huán)引用等。

Swift作為一個(gè)靜態(tài)編譯型語言,它摒棄了Objective-C中的Runtime機(jī)制。想要開啟動(dòng)態(tài)Property需要再Swift的Property前面增加聲明:dynamic,且使用dynamic必須是基于NSObject基類所構(gòu)造的類型,這樣做必然會(huì)喪失對(duì)Swift原始數(shù)據(jù)類型的支持,可見其是不好的。而且預(yù)計(jì)沒有多少朋友記得給變量打上dynamic的標(biāo)記吧,起碼我不會(huì)

很慶幸的是Swift語言在自己的Property中增加了getter/ setter的屬性觀察器,并對(duì)setter的屬性觀察器提供了willSet / didSet的兩個(gè)觀察器來詳細(xì)監(jiān)聽值的變化。這讓我們看到Swift本身是汲取了Objective-C在Runtime中創(chuàng)造的經(jīng)驗(yàn)和靈感,并將觀察者模式輕量化,以相當(dāng)優(yōu)雅的方式去表示一個(gè)值的變化過程。

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

可是我們?cè)陂_發(fā)中不僅僅是這樣的簡(jiǎn)單環(huán)境,我們需要針對(duì)MVVM中ViewModel開放一個(gè)被觀察者連接給ViewController,兩者產(chǎn)生聯(lián)動(dòng)。此時(shí)有人想到:"我提供一個(gè)閉包(block)設(shè)置給didSet就好了呀"。確實(shí)你可以這樣做。為每一個(gè)Property提供一個(gè)block雖然可行,但沒有重用好這一機(jī)制是則會(huì)讓代碼變得重復(fù)。那我們就要尋找一個(gè)好一點(diǎn)的方法來能讓Property變成一個(gè)被觀察者,當(dāng)它發(fā)生變更的時(shí)候,觸發(fā)一批block回調(diào)。

分析第三方:

ReactiveCocoa和RxSwift的第三方庫來實(shí)現(xiàn)是可以很好地實(shí)現(xiàn)觀察者模式(筆者更喜歡后者RxSwift的書寫風(fēng)格)。確實(shí),現(xiàn)在MVVM中采用RxSwift解耦作為中間件確實(shí)是產(chǎn)品開發(fā)潮流,這就像某種服裝搭配趨勢(shì)一樣的流行。那問題隨之而來,采用ReactiveCocoa和RxSwift都哪些共同缺點(diǎn)呢?我們開發(fā)實(shí)戰(zhàn)的時(shí)候肯定會(huì)遇到下面的問題:

  1. 訂閱和分發(fā)導(dǎo)致它本身的執(zhí)行效率低,會(huì)有大量的觸發(fā)棧和循環(huán)去進(jìn)行訂閱消息的分發(fā),遍歷逐個(gè)投遞。
  2. Swift本身的語法導(dǎo)致從Swift v.2 -> v.3 -> v.4的語法升級(jí)受制于蘋果的語言規(guī)則。Swift語言開發(fā)者的開發(fā)理念是快速激進(jìn)式的開發(fā)(我給它定名為:語法摧毀),雖然xcode提供了自動(dòng)化轉(zhuǎn)換語法功能,但難免會(huì)有轉(zhuǎn)換錯(cuò)誤和手動(dòng)修改的情況。這樣對(duì)于我們程序本身是非常不穩(wěn)定的變化,導(dǎo)致我們出現(xiàn)重寫程序組件的問題,甚至摧毀式的無法編譯
  3. ReactiveCocoa和RxSwift的開發(fā)成本比較高,語法體系“奇特”(碎片化的代碼,打散業(yè)務(wù)邏輯,由第三方庫限定語法編寫方式),導(dǎo)致團(tuán)隊(duì)間在合作時(shí)邏輯代碼理解難度加大。團(tuán)隊(duì)成員間的代碼溝通變慢。如果團(tuán)隊(duì)加入新人,學(xué)習(xí)成本則會(huì)提高。
  4. 庫文件升級(jí)緩慢,受制于他人,如果停止更新,可能你的產(chǎn)品就要趕緊尋找其他第三方庫來進(jìn)行重構(gòu)。

基于以上幾點(diǎn)缺點(diǎn),我在這里不贊同采用這樣的第三方組件的開發(fā)方式開發(fā),雖然它們很酷炫、顯得高大上!

全新創(chuàng)建:

那難道沒有一個(gè)又輕又容易維護(hù)的觀察者模式嗎?答案是有的!
那我們就從零開始一步步實(shí)現(xiàn)一個(gè)基于Swift 3~4的低調(diào)奢華有內(nèi)涵的觀察者模式(題外話由于我所書寫的日期是2017-6-6,正好是Swift 4發(fā)布當(dāng)日,我的工程文件又一次被Swift4的升級(jí)所摧毀,被摧毀的是第三方庫,那我還是自己造一個(gè)輪子吧!)

先來描述一下基本原理:

  1. 實(shí)現(xiàn)一個(gè)用于產(chǎn)生被觀察者的自定義泛型類:Observable<T>
  2. Observable自身提供blocks的閉包數(shù)組存放訂閱者的閉包
  3. 基于Observable中的value的setter方法,手動(dòng)調(diào)用每個(gè)閉包

先來看一下基礎(chǔ)代碼:

// 需要持有一批blocks,則必須創(chuàng)建一個(gè)類作為空間
class Observable<T> {
    typealias ObservableBlock = (T) -> ()
    private var blocks: [ObserverBlock] = []  // 持有blocks
    
    init(_ t:T) { self.value = t }  // 初始化value
    var value:T {
        didSet {
            // 實(shí)現(xiàn)didSet來遍歷block,觸發(fā)回調(diào)
            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"

代碼的運(yùn)行結(jié)果:

newValue: a
newValue: b

看到運(yùn)行結(jié)果,很不錯(cuò)!基于簡(jiǎn)單blocks持有,基于didSet就可以完成對(duì)于一個(gè)變量設(shè)置的變更監(jiān)聽。

繼續(xù)完善

仔細(xì)打量了代碼,中間缺少幾個(gè)能力:

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

第一步我們先來加入高級(jí)運(yùn)算符重載,片段代碼:

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

完整代碼:<a name="block_observable">[純block,可自動(dòng)釋放內(nèi)存]</a>

// 高級(jí)運(yùn)算符重載必須聲明在final頂級(jí)訪問級(jí)別的類中
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)
    }
}

/* 
定義 <- 運(yùn)算符 
運(yùn)算符定義必須放在文件級(jí)別當(dāng)中
*/
infix operator <-: ObservableChange
precedencegroup ObservableChange {
    associativity: left                 // 表示左結(jié)合
}

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"

代碼的運(yùn)行結(jié)果:

newValue: a
newValue: b
newValue: a

重載看上去還不錯(cuò),很精簡(jiǎn)!那繼續(xù)完善,填補(bǔ)后續(xù)的功能

第二步添加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  // !!!!!!!無法編譯,編譯報(bào)錯(cuò)!!!!!!!
            //報(bào)錯(cuò)信息:  Cannot check reference equality of functions;operands here have type '(T)->()' and '(T)->()'
        }

        self.blocks = blocksFiltered
    }
}

看到//報(bào)錯(cuò)信息: Cannot check reference equality of functions;operands here have type '(T)->()' and '(T)->()'
發(fā)現(xiàn)Swift中是不允許將兩個(gè)閉包進(jìn)行的比較的。雖然遺留的C API中是有unsafeBitCast可以對(duì)兩個(gè)閉包進(jìn)行比較,但我還是放棄這樣的寫法。

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

那既然block無法比較相等,就只能講上下文與blocks進(jìn)行綁定關(guān)系,來實(shí)現(xiàn)訂閱和刪除訂閱。

// 定義高級(jí)運(yùn)算符重載,必須為final訪問權(quán)限的聲明
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)
            }
        }
    }
    
    // 訂閱,創(chuàng)建觀察者元組
    func subscribe(observer:AnyObject, block:@escaping ObserverBlock) {
        observers.append(ObserverEntry(observer:observer, block:block))
    }
    
    // 解除訂閱,根據(jù)元組中的觀察者移除
    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                 // 表示左結(jié)合
}

// 運(yùn)算符重載
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"  // 取消訂閱,則不會(huì)看到"c"的打印

代碼的運(yùn)行結(jié)果:

oldValue:  newValue: a
oldValue: a newValue: b
oldValue: b newValue: a
// 這里沒有看到“c”

已知弊端:

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

  1. 就是插入式編程,
  2. 內(nèi)存循環(huán)應(yīng)用
    插入式編程就是會(huì)將原有的代碼的變量類型破壞,從而讓類型都趨向于Observable<T>數(shù)據(jù)類型,這樣喜歡純正變量監(jiān)聽的話,當(dāng)下除了willSet和didSet,尚未發(fā)現(xiàn)其他更優(yōu)雅的方法!

*** 而內(nèi)存循環(huán)應(yīng)用,需要將被保存在entry當(dāng)中的Observer在必要的時(shí)候unSubscribe掉才可以解決循環(huán)引用的問題。

2017-06-12后續(xù)

經(jīng)過測(cè)試我采用了自動(dòng)釋放和手動(dòng)釋放兩個(gè)方式編寫Observable源碼。而上面的代碼中,我將subscribe:Observer修改為block與ObserverName綁定的形式,來解決內(nèi)存循環(huán)引用的問題。
進(jìn)一步修改代碼我們來看一下:

// final class for operator <-
// 高級(jí)運(yùn)算符重載必須聲明在final頂級(jí)訪問級(jí)別的類中

public final class Observable<T> {
    typealias ObserverBlock = (_ oldValue: T, _ newValue: T) -> ()
    typealias ObserversEntry = (block: ObserverBlock, observerName:String?)
    private var observers: Array<ObserversEntry>

    init(_ value: T) {
        self.value = value
        observers = []
    }
    
    var value: T {
        didSet {
            observers.forEach { (entry: ObserversEntry) in
                let (block, _) = entry
                block(oldValue, value)
            }
        }
    }

    func subscribe(block: @escaping ObserverBlock) -> Self {
        let entry: ObserversEntry = (block: block, nil)
        observers.append(entry)
        return self
    }
    
    // set ObserverName for unsubscribe
    func addObserverName(_ observerName: String) {
        if observers.count > 0 {
            observers[observers.count-1].observerName = observerName
        }
    }
    
    // remove subscribe with ObserverName
    func unSubscribe(_ observerName: String) {
        let filtered = observers.filter { entry in
            let (_, observerNameSaved) = entry
            if (observerNameSaved != nil) {
                return observerNameSaved != observerName
            } else {
                return true
            }
        }
        
        observers = filtered
    }
}
/*
 定義 <- 運(yùn)算符
 運(yùn)算符定義必須放在文件級(jí)別當(dāng)中
 */
infix operator <-: ObservableChange
precedencegroup ObservableChange {
    associativity: left                 // 表示左結(jié)合
}

public func <- <T> (left: Observable<T>, right: T) {
    left.value = right
}

看到上方最新的代碼,我們可以觀察到添加了一個(gè)addObserverName(_ observerName:)用于給訂閱block注冊(cè)關(guān)鍵字,這樣就可以將一批訂閱者注冊(cè)并取消訂閱。且沒有內(nèi)存引用問題。思路我借鑒了RXSwift的調(diào)用時(shí)機(jī):addDisposeBag(disposebag:)
那么釋放的使用方法如下:

let example = Observable<String>("")
example.subscribe { (old:String, new:String) in
    print("oldValue:", old, "newValue:", new)
}.addObserverName("TheExampleName")
example <- "a"
example.unSubscribe("TheExampleName")

好了,經(jīng)過細(xì)細(xì)打磨的Observable已經(jīng)初步具備了觀察者能力了,并且可以輕巧的應(yīng)用于變量的觀察
全部代碼:
https://github.com/maxcong/Observable-Block-Swift

我在編寫期間試用了google的一個(gè)開發(fā)者開發(fā)的Observable-Swift的,
但這個(gè)只針對(duì)于Swift 3,功能略復(fù)雜了,最后放棄

此文拋磚引玉,希望看到的開發(fā)者如果有優(yōu)雅的方法可以在文章后面留言。深表感謝!

本文原創(chuàng) 作者:叢瑜帥 如需轉(zhuǎn)載請(qǐng)注明

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,050評(píng)論 4 61
  • 記 往昔 舊故里 充滿回憶 少年游經(jīng)歷 至今留戀不已 天真歲月多甜蜜 追蝶逐夢(mèng)童言無忌 然今夕何夕流年離棄 縱與世...
    一袍風(fēng)閱讀 334評(píng)論 4 1
  • 某一天的某個(gè)瞬間對(duì)日歷突然有了好感,似乎當(dāng)時(shí)的自己比現(xiàn)在忙碌一些,覺得寫下的事情就是提上了日程。一度徜徉,有時(shí)候也...
    一座云閱讀 273評(píng)論 0 0
  • 1. “尋找羅姓先生,黃色人種,煙灰毛衫,發(fā)茂盛,體...
    夢(mèng)囈島的魚小姐閱讀 430評(píng)論 1 0

友情鏈接更多精彩內(nèi)容