移動客戶端架構案例分析與思考
寫在前面
關於題目
分享之前,想說一下為什么選擇了“架構”這個主題,其實初衷有兩個:
第一,“架構”對於我們來說實在是太重要了,咱們雖然沒有架構師這個職位,但是在開發的時候,都需要先有個很好的設計,希望我們的代碼是易維護的,而“設計”往往都會落到“架構”上。所以希望這次分享能夠對於大家在架構設計上有一點幫助。
第二,即便“架構”如此的重要,大家再聊到“架構”這個話題的時候,還是感覺到有點“虛”。想想原因,可能是因為每個人對於“架構”的理解可能都不太一樣,一個人在不同階段對於“架構”的理解也會不一樣,架構設計還很依賴於實踐和經驗,很多設計細節(取舍)都是在實踐中不停的迭代、改進,進而反思才能得到價值觀的升級。所以我借這次機會,也將我自己之前零散的架構方面的理解,總結一下,爭取能形成一點體系上的認識。希望大家聊起架構時,能稍微不那么“虛”。
分享形式
為了不那么枯燥,今天分享形式,我選了3個架構的案例,來進行分析,來試圖講清楚我對於架構的認識,以及怎么樣設計出一個好的架構,當然這個話題太大了,我今天先給大家開個頭,希望先能有一點感覺就好。
最后希望大家帶着批判的精神來聽,歡迎多交流。
第一個案例:獵戶語音iOS SDK —— 架構是被業務驅動的
第一個案例,舉一個我們自己的,放在第一個來講,也是想強調“懂業務”對於設計出好的架構來說,是非常重要的,這也是一些人往往會忽略的點。
很多同學肯定想,在項目的一開始,就設計一個完美的架構,以后能 hold 住各種變化。其實這是不可能的,架構一定是隨着業務的變化,而不停的演化和進化的,在有的階段還可能對於之前架構做比較大的調整。
背景
大家都知道,我們同時維護了幾個App,比如小豹、小雅、錘子等,這些上層App,都要依賴於底層的一些framework,比如
- 業務中心 OrionServiceSDK.framework (包括很多主要業務)
- 信息流 OVSChat.framework
- 技能商店 OVSSkillStore.framework
- 聲紋 OVSVoicePrint.framework
- 等等
在早些的時候,我們對於 SDK 的拆分粒度比較細,比如一個“找手機”技能,都會做成一個 framework。當時的 framework,應該在20個左右,各個 framework(組件) 的依賴關系圖如下:
歷史原因
我們這么做當時的原因很簡單,希望能夠解耦各個組件,將組件盡量拆細,然后App方想使用什么功能都可以做到熱插拔,不多也不少,多好~
理想 VS 現實
理想和現實往往會出現矛盾,到實際應用的時候,這樣的設計就給我們帶來了問題。如果對每個 framework 進行編號,比如1到20,那么我們理想中是這樣的:
但是現實其實是這樣的:
對比兩張圖的含義就是,實現中我們各個App,使用的 framework 大體相同,我們即便把業務拆的很細,若干個被拆分的 framework 其實還總是綁在一起使用。並且,還給我們維護帶來了巨大的問題,光每次打Git tag,都要折騰一會,會感覺精力都花在了一起輔助工作上。
優化
針對這個問題,我們專門優化了 framework 的個數,將相似業務的 framework 進行了合並,最終 framework 的數量減少到了10個一下,組件之間的依賴圖變成這樣:
優化之后效果也很明顯,我們對於各個framework的維護,變得簡單多了。
兩個彩蛋
另外在優化的過程,有兩個值得一提的是:
- 之前“信息流“和”推送“還存在環狀依賴的問題,就會導致當你想把 ”推送“的framework合並到業務中心的時候,竟然還得讓業務中心去依賴信息流,這個當然是無法忍受的,解決辦法也比較經典,就是讓依賴下沉,把 Push 依賴的信息流的協議,放到了業務中心中。
- 這次的優化,其實是將”松散“變成了”耦合“,和我們平時常提到的觀點剛好相反,但是確實是我們當前甚至今后一段時間內做所以我想說的是,”耦合“其實只是一個特征,雖然大部分情況是缺陷的特征,但是當耦合成為需求的時候,耦合就不是缺陷了。(有沒有一點在哪里聽過的感覺)
小結
我們談”架構“的時候,說的最多的就是”取舍“,什么叫”取舍“,就是說你不能很簡單的就判別出哪個是好的,哪個是不好的,總是覺得有點左右為難。而如何取舍?業務就是非常重要的一個標桿,只有結合業務,才能判斷出哪個是最適合自己的。我們結合了業務,對自己的 framework 的數量進行了精簡,當然也可能會根據業務的變化,在未來某天,需要將現有的framework拆分的更細。
第二個案例:餓了么移動APP的架構演進 —— 形成體系的認識
你做的項目,技術架構是怎么樣的?
幾乎所有人在被面試或者面試別人的的時候,都會(被)問到這個問題,很多人會回答,我們架構是MVC(MVVM),少數人還會使用MVP或者VIPER,我們姑且都稱為MV(X),但是真的架構僅僅就是MV(X)嗎?其實我覺得MV(X)雖然是架構中比較重要的部分,但是還是遠遠不能說架構 = MV(X)。
為什么呢?帶着這個問題,我們來看第二個例子,在這個案例中,我們關注下面幾點:
- 架構是如何隨着業務的變化而變化的(這個也是對上面觀點的一個證實)。
- 我們談到架構就提的 MV(X),處於架構中的哪個部分。
- 通過”餓了么“的架構演變,體會一下每個階段的側重點是什么,對於架構有一個體系上的認識。
文章地址:
餓了么移動APP的架構演進
https://www.jianshu.com/p/2141fb0dc62c
”餓了么“的架構經歷了4個階段的演化:
- 第一階段 MVC
- 第二階段 Module Decoupled (組件化)
- 第三階段 Hybrid
- 第四階段 React-Native & Hot Patch
第一階段 MVC
這個古老而經典的模式,不用多說。它是一個軟件”從無到有“,”短平快“開發的首選。也是大部分規模比較小的 App 幾乎大部分時間精力都會與之打交道的一個架構,以至於人們提架構比彈MVC。
當然這個架構隨着業務的劇增,很快就會出現弊端,朝着Massive-View-Controller的方向奔去。
第二階段 Module Decoupled
隨着代碼量不斷增加,功能模塊越來越多,不管是分工開發協作,還是已有模塊的復用和維護,組件化都成了這個階段的重點。組件化有個兩個關鍵:
- 如何划分組件。
- 如何實現組件之間的通信。
對於第一個問題,”餓了么“采用的方案,基本是業界廣為使用的分類方案,將組件分為共有組件和業務組件,
- 公共組件提供了一些業務無關的基礎服務:比如網絡庫、數據庫、JSONModel等
- 業務組件則對應具體的一塊業務,比如登錄業務組件,訂單組件等
對於兩種組件的管理:
- 對於公共組件,使用CocoaPods進行版本管理(這點和我們目前不太一樣,因為我們是SDK提供方,我們引用的第三方庫,不確定我們的SDK使用方是否使用,是否更改源碼,所以我們的方式,是將穩定版本的源碼,混淆后打包進我們的代碼)。
- 對於業務組件,這個和我們大體類似,采取了業務模塊注冊機制的方式來達到解耦的目的,每個業務模塊對外提供相應的業務接口,再啟動時向一個中心注冊自己的Scheme(我們是協議)。
而在具體某個業務組件內部,則可以根據不同開發人員,不同隊伍的偏好,來實現不同的代碼架構,比如MVC、MVVM、MVP等,也都不會影響整體系統架構。
這時的架構圖,看上去長這樣:
我們可以看到,MV(X) 已經不是關注的全部了,很多模塊已經和 MV(X) 不怎么搭邊了。
所以說,架構不等於 MV(X),其實 MV(X) 關注的只是”應用層“的部分。
關於分層:
一般的,可以將App分為三層:應用層、service層、data access層。
- 應用層 是直接和用戶打交道的部分,我們常用到的 UIViewController,Android的 Activity,負責了數據的展示、流向、用戶交互的處理。
- service 層 是在應用層的下面,為應用層服務器的,對於應用層來說就像一個API調用延遲為0ms的Server API。一般會放在應用層的代碼:網絡接口調用、公共系統服務API(GPS定位、隱私權限訪問)、一些 UTil 代碼(所以我覺得比如一個 UIViewController 的一些私有方法和一些提工具性質的category,其實應該算serveice 層)。
- data access 提供和對於數據的”增刪改查“的接口層。
第三階段 Hybrid
業務的變化又來啦,當用戶規模達到比較大的數量,這次不僅僅是功能的增加,每兩周一版已經滿足不了產品、運營躁動的心了;同時,用純 Native 代碼編寫的 App,如果上線后有錯誤,只能等下一次提交市場。在如今互聯網競爭如此激烈的時代,一次線上錯誤有時也會帶來很大的影響。所以這時候,很多純粹展示性的模塊會使用 H5 的方式來實現。
但是這種方式也有它的弊端:
- 每次加載頁面需要請求服務器,渲染時間比較長。
- 調用本地硬件設備存在一定的不便。
對於這個問題,也有很多方案可以權衡,比如可以提前將網頁打包好,以減少網絡傳輸的時間,同時提供一系列的插件來訪問本地的硬件設備。
“餓了么”這里的做法是,綜合了 Native 和 H5 的優缺點,將頁面做了一個划分,純粹展示性的模塊使用 H5;而更多的數據操作、動畫渲染性的模塊使用 Native。
架構圖長成這樣子:
業務再一次再架構的演化中扮演了重要的角色。
第四階段 React-Native & Hot Patch
又要頻繁迭代,又要用戶體驗,這時就考慮到了RN;另外,餓了么這個階段用戶已經過億,線上一個小 bug 都可能影響幾萬人的使用,所以這個階段,重點在於 RN 模塊的引入,以及 Hot Patch 熱修復功能的引入。
在 RN 的使用方面,依然有一個取舍,要回答下面的問題:
- 哪些頁面使用 RN,哪些頁面不用 RN。
- 是整個模塊使用 RN,還是一個模塊的部分頁面使用 RN。
- RN 和 Native 頁面是2選1的關系,還是說是一個備份。
- RN 和 Native 頁面如何通信。
“餓了么”的做法是:對於20%最重要的頁面,做了一個 RN 的鏡像,也就是一個備份,然后通過服務器的配置,來切換Native 還是 RN,這樣如果 Native 頁面出現問題的時候,先通過開關將線上的頁面切換成 RN,先保證線上正常使用,然后使用 Hot Patch 完成修補后,再切換回 Native App 原生頁面。
這時的架構圖:
不得不說,這種做法不一定適合別的團隊,畢竟一個頁面,要寫 Native 、 RN 兩套代碼,並且要一直維護,花的代價都有點大,不是每個團隊都有精力去這么搞的。其實這點,也正說明了,你需要根據自己業務,設計出一個最適合自己項目的架構。
小結
小結一下:
- 業務一直在影響架構的變遷。
- MV(X) 其實只是“應用層”的事,對於架構應該有個系統的認識。
- 架構的設計,並不是有現成的拿來用就 OK 的事,還有很多細節的部分需要做取舍,依賴業務需求和經驗。
第三個案例:《猿題庫 iOS 客戶端架構設計》—— 好的架構具有哪些特質
第三個案例我們回歸 MV(X),畢竟它確實是我們日常開發接觸比較多的一部分。
對了這個案例,想關注的“點”是
- MVC 和 MVVM 的優缺點。
- 如何能夠規避缺點,結合優點,改進架構,設計一個適合自己的MV(X)架構。
- 這個思想的底層原理是什么,在別的場景下的設計能夠通用。
文章地址:
猿題庫 iOS 客戶端架構設計
http://gracelancy.com/blog/2016/01/06/ape-ios-arch-design/
MVC
優點:
- 易理解,對應現實生活中也是這樣的。
- 易上手,iOS、Android 默認就是個 MVC 的環境。
缺點:
- 當指責不是那么明確,不知道該放哪時,代碼就會被放在"Controller"里面吧,Controller越來越難維護。
其實對於上面這個缺點,唐巧也在一篇文章中寫道,這個問題其實也不能說是 MVC 的缺點,是我們沒有拆分好代碼。可以看看唐巧的《被誤解的 MVC 和被神化的 MVVM》,提出了一些如何解決 Controler 臃腫的解決辦法,然后也表達了對於 MVVM 的質疑,具體的做法可以去讀這篇文章。這也正說明了大家對於架構的理解和態度真的是有區別的。
MVVM
具體關於MVVM的概念可以參考 Objc 的《MVVM 介紹》,這里就不具體說 MVVM 的概念了。
不了解MVVM的同學,知道這幾點就行:
- MVVM將ViewController視作View。
- 關於 View Model,只需要知道兩件事:持有model;View可以完全通過一個View Model決定自己如何展示。
- View 和 View Model,View Model 和 Model之間通過數據綁定,使得 Model改變的時候,能同步更新 View Model,進而更新 View。
優點:
- 減輕了 Controller 的負擔,拆分了代碼
- View Model有比較好的測試性。
- 結合 RAC, 可以將數據和 View 通信的代碼精簡到很少。
缺點:
- 上手成本高。
- 由於使用數據綁定,界面的 bug 變的不易調試。
- ViewModel 接管了 ViewController 的大部分職責,慢慢也可能變的臃腫。
綜合兩者
來看下 Lancy 的設計,是如何將兩者綜合,規避缺點,保留優點的,先上圖:
對於上圖的說明:
- 一個View Controller 持有一個 Data Controller。
- Data Controller,是數據管理模塊,負責數據的生命周期:獲取、保存、更新。
- 一個 View Controller 里面有多個 View,每個 View 對應一個 View Model,這里的 View Model 概念和 MVVM 里的類似,唯一不同的是這里的 View Model 和 Model ,沒有綁定機制。
- View 的展示樣式,完全決定於 View Model。
結合產品 UI,再按照數據流的方式闡述,以下面的 CollectionView 為例。
- View Controller 持有 一個 Data Controller,初始化之后,調用 Data Controller 獲取用戶打開的課程。(1)
- Data Controller 通過 API 獲取數據,封裝成 Model 並返回 (2,3)
- View Controller 將2中返回的數據,生成 View Model,調用 View 的 bindDataWithViewModel 方法裝配給對應的 View。(4)
- View Controller 會調用 View 的渲染方法,View 通過 View model 直接進行渲染。(5)
- 如果有用戶事件,通過代理的方式,傳遞給 View Controller,讓View Controller 來決定下一步的處理。(6)
這么方式的優缺點:
優點:
- 指責分明,確定給 Controller 肩負。
- 耦合度低,測試性高。指責分明帶來的效果就是耦合度低,同一個功能,可以分別由不同的開發人員分別進行開發界面和邏輯,只需要確立好接口即可。
- 學習成本低,不用事件綁定,不需要學習 RAC。
- 易於調試 Bug,不使用綁定帶來的好處。
缺點:
- 當頁面的交互邏輯非常多時,需要頻繁的在 DC-VC-VM 里來回傳遞信息,造成了大量膠水代碼。
- 沒有綁定,帶來額外的代碼(綁定真的是雙刃劍)
小結
針對這個案例,我覺得最應該我們思考的就是,作者 Lancy,在設計架構的時候的思路是怎么樣的?為什么要那么設計?是怎么取舍的?總結一下:
- 因為想讓團隊能夠快速上手,以及bug可以快速調試,所以沒有使用綁定機制。(從學習成本、開發成本、以調試角度)
- 依然保證了指責的明確划分。(好的架構一定要明確划分職責,甚至均衡的划分)
- 方便測試依然是重要的一個設計標准。(好的架構要易於測試)
- 還有相當重要的一個標准——解耦(好的架構要易於維護,解耦意味着比較易於維護)
- 上面提到了缺點之一是“當頁面交互邏輯非常多時,會不太合適”,這也說明了,作者采用了這個架構,其實是基於頁面交互不是很多的情況(用戶交互確實帶來Model的改動不是很多,當前界面並不能修改用戶所開的課程)。所以業務依然是影響架構設計的總要因素。
- 還有一點不知道大家有沒有在意,上面提到了“數據流”,對着這個架構我們能清晰的說出“數據流”,這個我認為也是一個好的架構應該具有的特性。數據流如果很模糊,有很多分支,那我們的維護成本將大大增加,一個清晰的數據流,意味着你無論在這個流的那個節點繼續執行下需,都能得到正確的結果。
基於對這個案例的分析,最應該思考的是,設計一個架構的思路,換言之,你要心里明白,怎樣才是一個好的架構。
總結
總結一下,今天說的這三個案例,其實就是為了說明一下幾點:
- 懂業務對於架構的重要性。
- 架構 != MV(X),站在更加宏觀的角度看問題,對於打開思路更有幫助。
- 當我們設計架構的時候,怎樣才是一個好的架構。
其實“架構”真的是個很大的話題,很多知識都可以拿出來單獨學習和分享。
- 設計原則和設計模式(設計的基本功)
- 數據結構和算法(設計的基本功)
- MV(X)/Viper
- 組件化 (光這個就特別多可以講的)
- 網絡層的架構設計(比如離散的還是集約的)
- 持久化層的設計
- Hybrid 的設計
- RN、Hot Patch
- 無埋點
- pod 私有庫維護 SDK
- 面向過程/面向對象
- AOP
- 。。。。。
希望以后能陸續的為大家分享,擅長哪個方向,或者對哪個方向感興趣的小伙伴也可以給大家分享一下,讓大家的設計能力一點點提高上來。
參考
MVVM 介紹
https://objccn.io/issue-13-1/
被誤解的 MVC 和被神化的 MVVM
http://blog.devtang.com/2015/11/02/mvc-and-mvvm/
iOS應用架構談系列
https://casatwy.com/iosying-yong-jia-gou-tan-kai-pian.html
猿題庫 iOS 客戶端架構設計
http://gracelancy.com/blog/2016/01/06/ape-ios-arch-design/
餓了么移動APP的架構演進
https://www.jianshu.com/p/2141fb0dc62c
iOS應用層架構之CDD
http://mrpeak.cn/blog/cdd/
iOS應用架構現狀分析
http://mrpeak.cn/blog/ios-arch/
iOS 架構模式–解密 MVC,MVP,MVVM以及VIPER架構
http://www.cocoachina.com/ios/20160108/14916.html