【干貨】言簡意賅 Android 架構設計與挑選

重學安卓 3 周年集大成作,邀您一起回顧 Android 架構演變與選型故事。小專欄、掘金、公眾號同步發行,歡迎閱讀點贊收藏。

前言

談到 Android 架構,相信誰都能說上兩句。從 MVC,MVP,MVVM,再到時下興起 MVI,架構設計層出不窮。如何為項目選擇合適架構,也成常備課題。

由于架構并非空穴來風,每一種設計都有其存在依據。唯有高頻痛點熟稔于心,才能技術選型事半功倍。所以今天我們一起探尋 “架構演化” 來龍去脈,相信閱讀后你會豁然開朗。

文章目錄一覽

  • 前言
  • 原生架構
    • 原始圖形化架構
      • 高頻痛點 1:Null 安全一致性問題
    • 原始工程架構 MVC
      • 高頻痛點 2:成員變量爆炸
      • 高頻痛點 3:狀態管理一致性問題
      • 高頻痛點 4:消息分發一致性問題
  • 它山之石
    • 矯枉過正 MVP
      • 反客為主 Presenter
      • 簡明易用 三方庫
    • 撥亂反正 MVVM
      • 曲高和寡 DataBinding
      • 未卜先知 mBinding
  • 力挽狂瀾
    • 官方牽頭 Jetpack
      • 一舉多得 ViewModel
      • 讀寫分離 LiveData
    • 半路殺出 Kotlin
      • 喜聞樂見 ViewBinding
  • 百花齊放
    • 最佳實踐 Jetpack MVVM
      • 屏蔽回推 UnPeekLiveData
      • 嚴格模式 DataBinding
    • 前后通吃 Kotlin Flow
    • 消除樣板 MVI
    • 另起爐灶 Compose
  • 綜上

原生架構

原始圖形化架構

完整軟件服務,通常包含客戶端和服務端。

Linux 服務端,開發者通過命令行操作;Android 客戶端,面向普通用戶,須提供圖形化操作。為此,Android 將圖形系統設計為,通過客戶端 Canvas 繪制圖形,并交由 Surface Flinger 渲染。

但正如《過目難忘 Android GUI 關系梳理》所述,復雜圖形繪制離不開排版過程,而開發者良莠不齊,如直接暴露 Canvas,易導致開發者誤用和產生不可預期錯誤,

為此 Android 索性基于 “模板方法模式” 設計 View、Drawable 等排版模板,讓 UI 開發者可繼承標準化模板,配置出諸如 TextView、ImageView、ShapeDrawable 等自定義模板,供業務開發者用。

這樣誤用 Canvas 問題看似解決,卻引入 “高頻痛點 1”:View 實例 Null 安全一致性問題。這是 Java 語言項目硬傷,客戶端背景下尤明顯。

高頻痛點 1:Null 安全一致性問題

例如某頁面有橫豎兩布局,豎布局有 TextViewA,橫布局無,那么橫屏時,findViewbyId 拿到則是 Null 實例,后續 mTextViewA.setText( ) 如未判空處理,即造成 Null 安全問題,

對此不能一味強調 “手動判空”,畢竟一個頁面中,控件成員多達十數個,每個控件實例亦遍布數十方法中。疏忽難避免。

那怎辦?此時 2008 年,回顧歷史,可總結為:“同志們,7 年暗夜已開始,7 年后會有個框架,駕著七彩祥云來救你”。

原始工程架構 MVC

時間來到 2013,以該年問世 Android Studio 為例,

工程結構主要包含 Java 代碼和 res 資源??紤]到布局編寫預覽需求,Android 開發默認基于 XML 聲明 Layout,MVC 形態油然而生,

其中 XML 作 View 角色,供 View-Controller 獲取實例和控制,

Activity 作 View-Controller 角色,結合 View 和 Model 控制邏輯,

開發者另外封裝 DataManager,POJO 等,作 Model 角色,用于數據請求響應,

顯而易見,該架構實際僅兩層:控制層和數據層,

Activity 越界承擔 “領域層” 業務邏輯職責,也因此滋生如下 3 個高頻痛點:

高頻痛點 2:成員變量爆炸

成員聲明,動輒數十行,令人眼花繚亂。接手老項目開發者,最有體會。

高頻痛點 3:狀態管理一致性問題

View 狀態保存和恢復,使用原生 onInstanceStateSave & Restore 機制,開發者容易因 “記得 restore、遺漏 save” 而產生不可預期錯誤。

高頻痛點 4:消息分發一致性問題

由于 Activity 額外承擔 “領域層” 職責,乃至消息收發工作也直接在 Activity 內進行,這使消息來源無法保證時效性、一致性,易 “被迫收到” 不可預期推送,滋生千奇百怪問題。

EventBus 等 “缺乏鑒權結構” 框架,皆為該背景下 “消息分發不一致” 幫兇。

“同志們,5 年水深火熱已過去,再過 2 年,曙光降臨”

好家伙,這是提前拿到劇本。既然如此,這 2 年時間,不如放開手腳,引入它山之石試試(就逝世)。

它山之石

矯枉過正 MVP

這一版對 “現實狀況” 判斷有偏差。

MVP 規定 Activity 應充當 View,而 Presenter 獨吞 “視圖邏輯” 和 “業務邏輯”,通過 “契約接口” 與 View、Model 通信,

這使 Activity 職能被嚴重剝奪,只剩末端通知 View 狀態改變,無法全權自治視圖邏輯。

反客為主 Presenter

從 Presenter 角度看,似乎遵循 “依賴倒置原則” 和 “最小知道原則”,但從關系界限層面看,Presenter 屬 “空降” 角色,一切都其自作主張、暗箱操作,不僅 “未能實質解決” 原 Activity 面臨上述 4 大痛點,反因貪婪奪權引入更多爛事。

這也是為何,開發過 MVP 項目,都知有多別扭。

簡明易用 三方庫

基于其本質 “依賴倒置原則” 和 “最小知道原則”,更建議將其用于 “局部功能設計”,如 “三方庫” 設計,使開發者 無需知道內部邏輯,簡單配置即可使用。

Github:Linkage-RecyclerView

我們維護的 “餓了么二級聯動列表” 庫,即是基于該模式設計,感興趣可自行查閱。

撥亂反正 MVVM

經歷漫長黑夜,Android 開發引來曙光。

2015 年 Google I/O 大會,DataBinding 框架面世。

該框架可用于解決 “高頻痛點1:View 實例 Null 安全一致性問題”,并跟隨 MVVM 模式步入開發者視野。

曲高和寡 DataBinding

MVVM 是種約定,雙向綁定是 MVVM 特征,但非 DataBinding 本質,所以長久以來,開發者對 DataBinding 存在誤解,認為使用 DataBinding 即須雙向綁定、且在 XML 中調試。

事實并非如此。

DataBinding 是通過 “可觀察數據 ObservableField” 在編譯時與 XML 中對應 View 實例綁定,這使上文所述 “豎布局有 TextViewA 而橫布局無” 情況下,有 TextViewA 即被綁定,無即無綁定,于是無論何種情況,都不至于 findViewById 拿到 Null 實例從而誘發 Null 安全問題。

也即,DataBinding 僅負責通知末端 View 狀態改變,僅用于規避 Null 安全問題,不參與視圖邏輯。而反向綁定是 “遷就” 這一結構的派生設計,非核心本質。

礙于篇幅限制,如這么說無體會,可參見《從被誤解到 “真香” Jeptack DataBinding》解析,本文不再累述。

未卜先知 mBinding

除了本質難理解,DataBinding 也有硬傷,由于隔著一層 BindingAdapter,難獲取 View 體系坐標等 getter 屬性,乃至 “屬性動畫” 等框架難兼容。

有說 MotionLayout 可破此局,于多數場景輕松完成動畫。

但它也非省油燈,不同時支持 Drag & Click,難實現我們 示例項目 “展開面板” 場景。

于是,DataBinding 做出 “違背祖宗” 決定 —— 允許開發者在 Java 代碼中拿到 mBinding 乃至 View 實例 …… ??? 那 DataBinding 不 bind 個寂寞,Null 安全還管不管?

—— 鑒于 App 頁面并非總是 “橫豎布局皆有”,于是開發者索性通過 “強制豎屏” 扼殺 View 實例 Null 安全隱患,而調用 mBinding 實例僅用于規避 findViewById 樣板代碼。

至于為何說 mBinding 使用即 “未卜先知”,因為群眾智慧多年后即被應驗。

力挽狂瀾

官方牽頭 Jetpack

時間回到 2017,這年 Google I/O 引入一系列 AAC(Android Architecture Components)

一舉多得 ViewModel

其中 Jetpack ViewModel,通過支持 View 實例狀態 “托管” 和 “保存恢復”,

一舉解決 “高頻痛點2:成員變量爆炸” 和 “高頻痛點 3:狀態管理一致性問題”,

Activity 成員變量表,一下簡潔許多。Save & Restore 樣板代碼亦煙消云散。

讀寫分離 LiveData

而 Jetpack LiveData,通過 protected + mutable 設計,實現單向數據流,從而

解決 “高頻痛點 4:消息分發一致性問題”。

所謂單向數據流,即無論請求從何處發起,觀察者收到都是從 “唯一可信源” 內部鑒權后統一推送的 “只讀消息”,如此可避免 “多頁面、多觀察者” 獲取 “過時、不實、不一致” 消息。

注:對此如無體會,可參見《吃透 LiveData 本質,享用可靠消息鑒權機制》解析,本文不作累述。

半路殺出 Kotlin

并且這時期,Kotlin 被扶持為官方語言,背景發生劇變。

Kotlin 直接從語言層面支持 Null 安全,于是 DataBinding 在 Kotlin 項目式微。

喜聞樂見 ViewBinding

千呼萬喚,ViewBinding 問世 2019。

如布局中 View 實例隱含 Null 安全隱患,則編譯時 ViewBinding 中間代碼為其生成 @Nullable 注解,使 Kotlin 開發過程中,Android Studio 自動提醒 “強制使用 Null 安全符”,由此確保 Null 安全一致。

ViewBinding 于 Kotlin 項目可平替 DataBinding,開發者喜聞樂見 mBinding 使用。

百花齊放

最佳實踐 Jetpack MVVM

自 2017 年 AAC 問世,部分原生 Jetpack 架構組件至今仍存在設計隱患,

基于 “架構組件本質即解決一致性問題” 理解,我們于 2019 陸續將 “隱患組件” 改造和開源。

屏蔽回推 UnPeekLiveData

如,LiveData 根據官方描述,可分別用于 “末流推數據” 及 “頁面間通信” 場景。但粘性設定使 LiveData 更傾向于前者場景,在后者場景中易發生不符預期 “數據倒灌” 問題。

為此我們從頭梳理 “消息分發” 背景來龍去脈,得出以下結論:

項目語言 DataBinding 可變 State 狀態托管和保存恢復 單向數據流
Java 必用 ObservableField Jetpack ViewModel
Kotlin 可不用 可無 Jetpack ViewModel

也即,DataBinding 項目,頁面旋屏重建后,DataBinding 可從 ViewModel 拿取綁定的 ObservableField 重新渲染。粘性設定可有可無。

在非 DataBinding 項目,由于現如今 “單向數據流” 結構,末流邏輯皆是 LiveData Observer 回調中完成,因而須 LiveData 粘性設定,自動推送最后一次數據。

由此可見,粘性設定確有其適用場景。但,畢竟是 “mutable 系” 框架先驅,消息鑒權天賦在此,不善用豈不可惜?

于是我們考慮屏蔽其 “粘性設定”,專用于 “應用內 - 頁面間 - 生命周期安全 - 來源可靠 - 只讀一致” 消息分發,并開源至 Github:UnPeekLiveData 集思廣益。

期間 “騰訊音樂” 小伙伴貢獻過 v5 版重構代碼,用于月活過億 “生產環境” 痛點治理。

嚴格模式 DataBinding

此外我們明確約定 Java 下 DataBinding 使用原則,確保 100% Null 安全。如違背原則,便 Debug 模式下警告,方便開發者留意。

具體可參見 Github:KunMinX-MVVM 使用。

前后通吃 Kotlin Flow

通常 Flow 可用于領域層、數據層,實現復雜數據變換與傳遞。

2021 官方考慮 Kotlin Flow + Lifecycle.repeatOnLifecycle 取代 LiveData。眼見 “末流推數據” 場景被平替,跟風抹殺 “消息分發場景可行性” 亦不絕于耳,諸如 “LiveData 設計之初就不是為這個用”。對此其實大可不必。

鑒于 Kotlin Flow “生產者消費者” 隊列設計,可用于諸如 “618 搶單” 等暴力測試場景。 Java 下 mutable 系唯 LiveData 可用,且常規操作 LiveData 足矣。

消除樣板 MVI

顯然 Kotlin 開發者還可再進一步,于 “表現層” 和 “領域層” 使用 MVI 設計。

MVI 基于 sealed class 加持,可集中接收本頁面 events 并分流具體 event。如此從 “唯一可信源” 角度看,“mutable 先驅” 被縮減為唯一實例,從而 mutable/immutable 樣板代碼縮減為一,不再百忙出錯。

不過,樣板代碼手寫出錯,其實易解決,例如 Java + MVVM 開發者完全可通過 “自動化工具” 生成樣板代碼,最簡單辦法即是在 Android Studio 中寫個 main 函數,循環拼裝輸出代碼。

此外 “單向數據流” 未能杜絕 “末端邏輯” 隱患,易在 “邏輯閉環誤被打破” 情況下,引發 “請求響應遞歸循環”。過去兩年讀者群有過數起類似事故討論,MVI 同有概率遭遇此問題。

所以,MVI 使用見仁見智,Java 開發者建議 Jetpack + MVVM + 自動化。

另起爐灶 Compose

回到文章開頭 Canvas,為實現 View 實例 Null 安全,先是 DataBinding 框架,但它作為一框架,并不體系自洽,與 “屬性動畫” 等框架難兼容。

于是出現聲明式 UI,通過函數式編程 “純函數原子性” 解決 Null 安全一致。且體系自洽,動畫無兼容問題,學習成本也低于 View 體系。

后續如性能全面跟上、120Hz 無壓力,建議直接上手 Compose 開發。

注:關于聲明式 UI 函數式編程本質,及純函數原子性為何能實現 Null 安全一致,詳見《一通百通 “聲明式 UI” 掃盲干貨》,本文不作累述。

綜上

高頻痛點1:Null 安全一致性問題

客戶端,圖形化,需 Canvas,

為避免接觸 Canvas 導致不可預期錯誤,原生架構提供 View、Drawable 排版模板,

為解決 Java 下 View 實例 Null 安全一致性問題,引入 DataBinding,

但 DataBinding 僅是一框架,難體系自洽,

于是兵分兩路,Kotlin + ViewBinding 或 Kotlin + Compose 取代 DataBinding。

高頻痛點2:成員變量爆炸

高頻痛點3:狀態管理一致性問題

引入 Jetpack ViewModel,實現狀態托管和保存恢復。

高頻痛點4:消息分發一致性問題

消息分發難追溯、過時、不一致,

mutable + 唯一可信源 “單向數據流” 解決,

但 mutable 滋生大量樣板代碼,于是局部 MVI,

但 “單向數據流” 難杜絕請求響應 “無限循環” 隱患,所以,天下無完美架構,唯有高頻痛點熟稔于心,不斷死磕精進,集思廣益,迭代特定場景最優解。

相關資料

Canvas,View,Drawable,排版模板:《過目難忘 Android GUI 關系梳理》

DataBinding,Null 安全一致,ViewBinding:《從被誤解到 “真香” Jetpack DataBinding》

LiveData,讀寫分離,消息鑒權:《吃透 LiveData 本質,享用可靠消息鑒權機制》

架構組件解決一致性問題:《耳目一新 Jetpack MVVM 精講》

MVI,集中管理,消除樣板代碼:《MVVM 進階版:MVI 架構了解下》

Compose,純函數原子特性,Null 安全一致:《一通百通 “聲明式 UI” 掃盲干貨》

版權聲明

Copyright ? 2019-present KunMinX 原創版權所有。

如需 轉載本文,或引用、借鑒 本文 “引言、思路、結論、配圖” 進行二次創作發行,須注明鏈接出處,否則我們保留追責權利。

本文封面 Android 機器人是在 Google 原創及共享成果基礎上再創作而成,遵照知識共享署名 3.0 許可所述條款付諸應用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容