前言
不知不覺加上實習和正式入職36氪已經半年左右,從第一次去實習以及接觸較為正式的軟件開發,對很多事情的懵懵懂懂,到琦哥讓我自己去全新設計我們基於Swift重寫的下一個大版本的底部交互,雖然物理時間比較短,卻感覺已經走過了很長的一段路。在這之間,總覺得有很多東西想總結一下,特別是這次設計底部交互的邏輯以及在切向Swift過程中對這個語言的進一步的體會。
這是第一次除了公司內部分享外寫技術文章,除了因為覺得這些感悟有分享的價值之外,可能更希望和別人碰撞出一些思維的火花,讓我們在開發的過程中繼續改進這種架構。雖然毫無疑問我認為自己設計的這套架構是很優雅的,但是確實也沒有看到別的程序采用這麼多的基礎數據類型.對很多問題的解決思路都是根據優雅做判斷, 對性能的判斷也是模模糊糊的基於在國外 Mooc 平台上的幾門介紹計算機底層的公開課上收獲的一些東西做出的.
實話說我本來想先寫總體的架構(也是第一個完成的事情), 但是因為我只是設計了對我們客戶端業務區的對外接口封裝, 內部實現大部分都是選用公開的第三方庫. 所以, 對裡面的實現並不能做到百分之百的把握(實話說, 我覺得一個人不自己寫一個那種庫,只是看看代碼,真的很難說自己了解那個東西,計算機太容易給一個人造成我懂了的假象, 但是實際上懂的人也許沒有那麼幾個).綜合以上, 打算自己在後面的時間利用晚上和周末, 自己實現那些東西的底層(這裡所說的底層,是說自己利用蘋果自己的框架提供的接口)之後, 再逐步的分模塊的介紹每一個部分的架構,總體的架構要放到最後面去寫了.
說明
這篇文章在講一些設計決策的時候, 會涉及到一些底層的考慮, 我不能保證這些講解都是百分百正確的, 因為它們都是建立在我以前上的課程和實驗的基礎上, 有很多都已經模糊了, 但是又不想查資料寫一些無關痛癢的簡介(請原諒我的懶惰)!雖然我現在在學一些反編譯和黑客技術, 力求能夠從底層的層面徹底搞清楚這些東西,但精力有限, 也只是剛剛開始.總之,大家帶著批判的思維來閱讀這篇文章,歡迎指出裡面的錯誤.
程序示例和 Cupid 庫的代碼在下面, 程序示例包含我們這一部分底部構建的全部代碼!
Cupid
正文
業務區的搭建
我做東西一般不會一開始就去選擇第三方庫, 因為覺得看別人的東西特別是做東西的時候看別人的東西容易給自己造成誤導, 做出一些並不是很適合自己的客戶端的設計決策.所以我一般都會根據自己的業務區先設計具體的上層接口, 然後再去選用第三方庫, 看哪一個能夠比較好的實現自己的接口.
下面是我們客戶端的上層接口的示例圖, 因為我們主要采用的是通過 OAuth 獲取認證信息, 然後讓服務器拉用戶數據的方式; 所以可能和你們的客戶端的方式不是很一樣.
業務區具體代碼可以從 Github 的代碼實例中查看. 但是可以從這個圖示中可以看到設計的大體思維方式, 就是更多的使用 Swift 中枚舉和結構體這兩種基礎數據類型, 然後使用閉包做回調, 減少使用類這種引用類型; 結構體扮演的角色主要是信息的流通通道和較為復雜單一的數據封裝, 枚舉則主要作為輕量型的數據傳遞; 雖然類仍然是業務區的中流支柱,畢竟無論是 MVC, MMVC, VIPER等各種架構 程序的方式, 都無法繞開蘋果提供的最上層的 Framework, 而那些仍然大部分是基於 NSObject 的.
為什麼我更喜歡枚舉和結構體,下面你會看到,越往下其實用到的類會越少.一個很重要的原因可能就是個人偏好, 有時候我也在想是不是用 Tuple 會是更好的解決方案, 但是因為對計算機底層技術的了解有限, 無法做出這種決策. 真的期待哪位大牛能寫一本叫做 Swift, The Good Parts 的書, 高級語言的靈活性對編程的泛濫確實並不是那麼好, 現在越來越少的能夠看到讓自己能夠眼睛一亮的代碼了.還有一個原因,就是使用基礎數據類型不用擔心內存洩露的問題, 而且一個 run loop 完成後, 整個過程中調用棧會自動的被清空, 實際上,在寫 Cupid 的過程中, 碰到幾次因為一行代碼的原因, service Provider 被提早釋放, 造成 completion handler 沒有被調用. 記得前一段時間因為學 Hand off 和 state restoration 技術, 用 Swift 翻譯了蘋果的 PhotoOff demo, 然後將它的下面的數據交互更改為 Struct + NSCache 的實現, 最後穩定狀態下內存占用量大概只有原來的一半, 內存洩露數量從60多處降到了10處不到(當然, 我覺得和 Swift 的實現更加安全也有一定的關系), CPU 的使用效率也更加的高效.最後, 越往下面的東西, 可能涉及到的數據操作會更少, 因為真正的數據在服務器或者數據庫的底層才會操作, 而數據庫的底層自己寫可能真的沒有成熟的第三方實現更加高效.蘋果自己的 News 都是使用第三方的 FMDB. 這些地方更多的是擔當一個信息流通的通道, 並沒有必要使用類這種重量型的數據類型(雖然, 現在編譯器的優化已經讓類和結構體的性能影響可以忽略), Swift 的語言特性, 讓結構體+枚舉+閉包的實現更加優雅!
業務區和 Cupid 庫的交互
業務區和 Cupid 的交互可能就比較簡單了, 因為 Cupid 存在的目的就是為業務區服務的, 至少之所以選在這個庫最主要的原因是它可以更好地為業務服務, 當然, 開發一個庫的目的是為了更適應普遍大眾(開發者)的需要.實際上, 我一開始的實現是使用 MonkeyKing, 但是因為公司業務需要我們沒有辦法去掉支付寶的分享功能, 在仔細審查 MonkeyKing 的代碼後,覺得並不適合這種業務需求才決定重寫一個自己的庫, 十分感謝 MonkeyKing 的開發者 @nixzhu 和@Limon-catch, 實際上, 很多 ServiceProvider 的實現方法都是 Copy 的 MonkeyKing 的代碼, 當然, 為了適應自己的邏輯做了很多的修改. 在下面的一節會詳細的介紹 Cupid 庫和其它分享庫的核心區別.
下面是業務區和 Cupid 的交互圖:
這一塊全部實現可以在 Github 的代碼實例中查看, 就不多做介紹.
Cupid 內部庫的構造
其實 MonkeyKing 寫的很好, 但是可能有人問, 為什麼有一個很好的東西, 還要自己再花時間去寫一個一樣的東西, 答案當然是, 他們不一樣! Cupid 從內到外都采用了完全不同的實現方式, 它的一些優點可以在主頁上查看, 我覺得 Demo 也很優美, 從界面到代碼.
內存結構
下面一張圖是 Cupid 的交互和其它第三方分享庫和核心的區別:
需要注意的是這個上面關於調用棧的思維是推測的, Swift 中如何處理結構體和枚舉對我來說還是一個黑箱子; 我覺得假如我是編譯器的作者就會采用這種思維方式.通過這種設計思維, 可以很有效的降低程序中單例類數量, 更好的利用內存空間, 特別是對於我這種一年分享不了幾次的用戶.
數據類型
之所以選擇重寫 MonkeyKing 還有一個很重要的原因就是我們的應用要支持剪切板和支付寶的分享, 我覺得基於 enum 封裝大批量數據的思維雖然調用的簡潔度可以更高, 但是在可擴展性上面並沒有做到更強. 所以 Cupid 選擇了基於 Protocol 的方式來實現特定的 Service Provider, 如果你們看源代碼的話, 可能會發現實現一個特定的 Service provider是十分的 easy 的, 而且你不需要更改任何其它的地方, 可以做到絕對的兼容.
本來我在 Service Provider 基於基類實現和基於接口實現做了一些取捨, 最後之所以選擇基於接口實現除了因為我不喜歡各種 override, 還有一個原因是因為每一個 Service Provider都有各種特定的需求, 是很獨特的, 基本上沒有任何共同的特點.
下面是 Cupid 處理一個分享或者認證的調用路徑, 這樣就不用像很多的第三方分享一下, 要 register 賬戶, 而且維持許多的單例:
結束語
就像一開始的說明,這裡面的很多底層交互都是自己的猜測或者說自己覺得會那麼實現底層,和實驗的結果, 請用批判的角度理解這裡面的內容, 在自己的工程中靈活的使用這些東西.如果各位讀者有了解裡面細節的, 歡迎補充!