【乾貨】言簡意賅 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 許可所述條款付諸應用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章