做iOS開發的同學都非常熟悉代理模式,為避免代碼耦合,代理模式的委托者任務交給代理執行,代理執行完畢之後再把回調告訴委托者。委托者不關心代理是怎麼執行任務的,隻關心結果是成功還是失敗。代理模式就像是殺手與雇主的關系一樣。
但是代理模式也不完美,代理多了,雇主也管不過來了,委托在A處,收結果卻要在B處。有的時候,雇主也希望能在同一個地方既可以發配任務,也可以接收結果。閉包Block就能幫雇主解決這個問題了。無論是系統的GCD,還是平時随手封裝一個 UIAlertView 的block實作,都讓代碼的可讀性有了一定的提升。
無論是代理模式,還是閉包,在處理單一任務的時候,都出色的完成了任務。可是當兩種模式要互相配合,一起完成一系列任務,并且每個任務之間還要共享資訊,互相銜接,雇主就要頭疼了。當然可以隻用一種模式來實作,代理模式就不說了,過于分散,不善于處理這種流程性的事務。那我用閉包來舉一個例子:我們需要順序執行Task A、B、C 三個任務,A、B、C依次執行,任務完成之後都使用閉包來回調并開始下一個任務。代碼如下:
上面的代碼看起來挺清晰,可讀性也還可。如果加上一些 ifelse 的分支判斷,再加上一些參數的傳遞,代碼不知不覺的向右延伸,最終超出了螢幕的寬度,形成一個倒金字塔的形狀。寫 JavaScript 的同學會說:你已經掉進了回調陷阱(CallbackHell),趕緊用Promise設計模式來跳坑吧。
Promise設計模式把每一個異步操作都封裝成一個Promise對象,這個Promise對象就是這個異步操作執行完畢的結果,但是這個結果是可變的,就像薛定谔的貓,隻有執行了才知道。通過這種方式,就能提前擷取到結果,并處理下一步驟。
Promise 使用 then 作為關鍵字,回調最終結果。 then 是整個Promise設計模式的核心,必須要被實作。另外還有其它幾個關鍵字用來表示一個Promise對象的狀态:
pending: 任務執行中,狀态可能會進入下面的fullfill或者reject二者之一
fufill/resolved: 任務完成了,傳回結果
reject: 任務失敗,并傳回錯誤
更多可以參考 官方規範(https://promisesaplus.com/ ) 。

如上圖所示,fullfill與reject的狀态都是不可逆轉的,保證了結果的唯一性。
除了 then ,一些對 Promise 的實作還有幾個關鍵字用來擴充,讓代碼可讀性更強:
catch: 任務失敗,處理error
finally: 無論是遇到 then 還是 catch 分支,最終都會執行的回調
when: 多個異步任務執行完畢之後才會回調
Promise設計模式在 iOS/MacOS 平台的最佳實踐是由大名鼎鼎的homebrew的作者 Max Howell 寫的一個支援iOS/MacOS 的異步程式設計架構 – PromiseKit , 作者的另一個廣為人知的趣事是因為沒有寫出反轉二叉樹而沒有拿到Google的offer。
我們先抛出對上面改良函數使用PromiseKit的實作,再看原理:
調試後,發現執行的結果與我們期待的一緻,但是上面的代碼對我來說有幾個疑惑點:
then 是怎麼串起來的;
怎麼實作的順序調用;
如果傳遞參數,參數是怎麼傳遞的。
帶着問題,來看Promise的源碼:
如果對Block不是很熟悉,可能不太了解這段代碼,實際上,PromiseKit靈活的使用了Block作為函數的傳回值來實作鍊式調用。相比原來的Block嵌套模式,PromiseKit使用Block将多個 then 串聯起來,解決了Callback Hell。
接着來繼續看下一個問題。
代碼有點長,不過也可以了解。這個方法是上面的thenon調用的,接受兩個參數,第一個參數是一個resolve的block,第二個參數是一個pending的block。一個Promise在執行完畢之後,無論狀态是變成resolve還是pending,都通過這個方法,執行對應的 then,并傳回一個Promise對象。上面的函數中,有一個dispatchBarrierSync,barrier是栅欄的意思,一般來說如果我們有多個異步任務,但是希望他們按照一定的順序執行,就可以使用這個方法。在這裡PromiseKit通過barrier實作了then的依次調用。在這個barrier方法内部,一個是會去看目前是否已經有下一個要執行的Promise,如果沒有就生成一個新的,另一個把對應的pending 放到handler隊列,依次執行。
這裡需要思考的另外一個問題是,既然多個任務之間有依次調用的關系,那麼這樣的一種任務流之間如何互相通信呢?PromiseKit用了一個比較有趣的辦法來實作相鄰Promise對象的參數傳遞。
在萬物皆消息的OC語言内部,每一個方法,包括Block在内都是有類型簽名的。這個類型簽名對象就是 NSMethodSignature
那麼對于block,怎麼擷取類型簽名呢?PromiseKit自己定義了一個block的結構體:
熟悉block的同學都知道,flags按照bit位儲存了一些block的附加資訊,在 <code>1<<30</code>的這個bit可以找到是否有類型簽名signature,剩下的就是通過flags移動指針,找到signature所在的記憶體空間了。找到了signature,也就擷取到了參數個數與函數傳回值這些資訊。函數傳回值的類型是經過編碼的,具體的對照表可以參考官方文檔(https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html )。
有了函數簽名,就能知道block的資訊了。上面隻截取了部分代碼,簡單來說,PromiseKit 通過動态的擷取block的參數個數與傳回類型來決定block的調用。一般來說, fullfill(id) 在調用的時候最多隻支援傳遞一個參數,在必要的時候,PromiseKit把這些參數放在一個數組裡面,這個數組就是 PMKArray ,當檢測到這個參數是一個數組的時候,就依次取出數組内的元素作為參數傳遞。
進而支援了多個參數的傳遞。
至此, 對PromiseKit的一些解釋也就結束了,PromiseKit有OC的1.0版本,也有支援了swift的3.0版本。如果你非常享受這樣的書寫方式,可以接入很多擴充的版本,可以寫出看起來優雅又舒服的代碼,比如 NSURLSession :
還有很多的擴充與關鍵字的支援,這裡都不再展開。
而對于我來說,Promise設計模式能夠解決我對散落在各處的代理模式産生的代碼的煩惱,也讓我避免了跳進回調陷阱,就值得總結了。
内容轉載自騰訊課堂 Coding 學院