作者: madroid
回顧
根據 App 行為的不同,我們對其進行分離/分層并確定其職責,每層之間的通訊交互采用響應式方式。
App 有三層結構,分別為 UI Layer、Domain Layer、Data Layer,其依賴關系是單向的,上層可以依賴下層,下層卻不能反過來依賴上層。大致如下,其中 Domain Layer 是可選層:
△ App Arch Layer Design
每層的主要職責分別為:
1、UI Layer: 使用 UI 元素展示 App 中數據
- 將底層數據處理成容易被 UI elements 使用的 Uistate 數據;
- 根據 UiState 繪制對應的 UI elements;
- 響應用戶的操作事件,根據需求對其進行分發;
2、Domain Layer: 封裝通用的業務邏輯
- 封裝復雜的業務邏輯,避免出現大型類;
- 封裝多 ViewModel 通用的業務邏輯,避免代碼重復;
3、Data Layer: 封裝統一的數據來源,提供單一可信來源
- 定義不同的 DataSource 來封裝 Framework 及三方 SDK 的 API;
- 定義 Repository 來整合相同業務的不同數據類型的 DataSource;
每層依賴關系是單向的,UI Layer 可以依賴 Domain Layer,但是 Domain Layer 卻不能依賴 UI Layer。這種依賴方式可以使用簡單的函數傳遞依賴事件,但是卻不能處理結果的回調,即 UiState 的更新。想要處理結果的回調每層之間就可以采用數據驅動/響應式的方式來交互了。這種方式也被稱為是單向數據流的方式,即 UI 事件從 UI 層流向數據層,UiState 從數據層流向 UI 層。
關于 UI Layer、Domain Layer、Data Layer 中更多詳細內容可以查看官方文檔應用架構指南:
https://developer.android.google.cn/jetpack/guide
與 MVI 的關系?
MVI 的全稱是 Model-View-Intent,這里的 Intent 并不是指 Android 中的 Intent 類,而是表示一種意圖,可以簡單理解為對用戶 Event 的一種抽象。其交互圖大致如下:
MVI 并不像 MVC、MVP、MVVM 一樣,不論是 Controller、Presenter 還是 ViewModel 都是 View 與 Model 之間的橋接類,負責這兩者之間的通信與交互 (雖然 MVC 可以跨過 Controller 直接進行交互)。而 Intent 并沒有類似的職責,僅僅是約束了 View 的事件通過類似枚舉的方式定義,這種方式更像是前端框架中的 Flux 或者是 Redux,更多內容可以查看 Reclaim the reactivity of your state management, say no to imperative MVI,實現 MVI 的主流框架有: Orbit、Mavericks、Uniflow-kt、Mobius。
- Reclaim the reActivity of your state management, say no to imperative MVI
https://zhuinden.medium.com/reclaim-the-reactivity-of-your-state-management-say-no-to-imperative-mvi-3b23ca6b8537
- Orbit
https://github.com/orbit-mvi/orbit-mvi/blob/06e9f759a87e7192767baeebc682fc92369a7eff/orbit-core/src/commonMain/kotlin/org/orbitmvi/orbit/internal/RealContainer.kt#L74-L75
- Mavericks
https://github.com/airbnb/mavericks/blob/e8a631a19fc1b044da3ddff358712e129dc487a6/mvrx/src/main/kotlin/com/airbnb/mvrx/CoroutinesStateStore.kt#L57-L59
- Uniflow-kt
https://github.com/uniflow-kt/uniflow-kt/blob/a1fdbeb733a0b550a162227be3b1e03d03197023/uniflow-core/src/main/kotlin/IO/uniflow/core/flow/ActionReducer.kt#L28-L32
- Mobius
https://github.com/spotify/mobius
有的 MVI 在實現還需要借助 ViewModel,僅僅是把 View 的事件定義成的對應的密封類。目的僅僅是為了強制實現單向數據流的方式,根據之前介紹實現單向數據流的方式還是比較簡單的,上層只能依賴下層實現,下層的處理結果通過 LiveData、Flow 方式更新。
那再來聊一下 MVC、MVP、MVVM 與 Android 官方的推薦的 MAD Arch 之間的關系。其實經常提到的 MVVM 與 Android 官方的架構還是有本質區別的。MVX (對 MVC、MVP、MVVM 的統稱) 的架構方式對 Model 這一層提到的非常少,留下的印象可能就是除了 VX 之外剩下的就是 Model 的部分。但是這部分在整個 App 的架構中也是非常重要的。我們還是有大量的業務邏輯是在 Model 層處理的。
而 Android 官方的架構中卻包含了這部分的描述,新增了 Data Layer 與 Domain Layer。所以總結下來就是 MVX 處理的僅僅是 UI Layer 中的問題,描述的是狀態管理的部分;官方文檔中描述的確是整個 App 的架構,是一種包含的關系。
如何處理線程?
無論是在哪一層都要確保其在主線程安全的,即在主線程調用不會阻塞主線程或者是拋出異常。那應該是在哪一層進行處理吶?其可選項有 ViewModel、UseCase、Repository、DataSource,只要在任何一層處理耗時操作都可以確保其是主線程安全的。這里建議采用 "就近原則",即誰產生數據誰就保持數據的安全性。
Data Layer 中 DataSource 是 "產生" 數據的地方,在這里直接切換到對應的子線程是可以的,代碼大致如下:
class NewsRemoteDataSource( private val newsApi: NewsApi, private val ioDispatcher: CoroutineDispatcher) { /** * 在 IO 線程中,獲取網絡數據,在主線程調用是安全的 */ suspend fun fetchLatestNews(): List<ArticleHeadline> = withContext(ioDispatcher) { // 將耗時操作移動到 IO 線程中 newsApi.fetchLatestNews() }}
如果 Repository 中需要整合很多的 DataSource 中的數據,在 Repository 中切換到對應的子線程也是可以的,這樣可以減少頻繁的線程調度。
同時也需要考慮響應業務的生命周期情況,如果當前業務跟隨這頁面進行的,那么使用 viewModelScope 或者是 lifecycleScope 即可;如果其業務是跟隨 App 的什么周期的,那么則需要使用整個 App 生命周期的 CoroutineScope;如果在 App 被終止后,仍然希望可以執行任務,那么可以考慮使用 WorkManager:
https://developer.android.google.cn/topic/libraries/architecture/workmanager
如何處理實體類 (Entity)?
各層之間的 Entity 根據其職責定義會有所不同,可以根據具體的使用場景自定義 Entity。如云端返回的 Entity 與數據庫需要存儲的 Entity 可能并不相同,使用相同的 Entity 會導致代碼的可維護性下降,而且沒有必要暴露過多的細節。如下:
@Entity(tableName = "user")data class RemoteUser( @PrimaryKey @SerializedName("user_id") val userId: String, val username: String, @Ignore val token: String, @Ignore val inventory: RemoteInventory, @Ignore val profile: RemoteProfile,)
這種場景下,我們就可以針對云端返回數據與數據庫存儲數據分別定義不同的 Entity,如下:
// 云端數據 Entitydata class RemoteUser( @SerializedName("user_id") val userId: String, val username: String, val token: String, val inventory: RemoteInventory, val profile: RemoteProfile,)// 數據庫 Entity@Entity(tableName = "user")data class UserEntity( @PrimaryKey val userId: String, val username: String,)
對于不同頁面直接傳遞數據的場景 (Intent),建議定義單獨的 Entity,因為傳遞數據的大小是有限的。定義大致如下:
@Parcelizedata class Inventory( val id: UUID, val type: String): Parcelable
對于 UI Layer 中的實體定義,要根據其業務類型進行細分,切記不要將一頁面中的所有的 UiState 都定義在同一個 Entity 中。因為匯總型的定義在相關字段的更新頻率不一致的時候會導致頻繁的 UI element 重復繪制,同時不可變的 Entity 的字段增加也會導致不必要的內存開銷。如果一個 UiState 中有超過 5 個狀態,那就需要回過來看下 UiState 是否可以進行拆分了。
UiState 中經常遇到的一個場景就是添加 Loading 狀態,這種情況添加封裝統一的 Wrapper 類進行處理,如下:
sealed interface UiStateWrapper { object Loading : UiStateWrapper class Success<T>(val uiState: T) : UiStateWrapper class Failure(val exception: Throwable) : UiStateWrapper}
這種處理方式,并不需要在 UiState Entity 新增一個 isLoading 字段,保持 UiState 的 "純潔性",同時也可以在 UI elements 中對 UiStateWrapper 做統一的處理,不必每個 UiState 中都出 Loading 的狀態,當然,這是在 Loading 處理邏輯相同的前提下的。
整體而言,根據不同職責定義不同的 Entity 會讓我們的代碼邏輯相對合理,但是會增加一定的工作量以及會對要使用何種 Entity 產生混淆。所以還是需要根據自己的項目及團隊情況決定是否需要精細化管理 Entity,大型團隊建議采用這種方式。
如何組織代碼?
代碼建議按照業務模塊方式進行組織,而非功能進行組織。大致如下:
# DO- Project - feature1 - ui - domain - data - feature2 - ui - domain - data - feature3
不要使用如下的方式:
# DO NOT- Project - ui - feature1 - feature2 - feature3 - domain - feature1 - feature2 - feature3 - data
采用 Feature 方式組織代碼的優勢大致有以下幾點:
- 我們大概率都是在已有的項目中開發,而歷史的項目中或多或少存在著一些歷史技術債務,我們可以在開發特性的時候引入新的技術,這樣不會對舊的目錄結構產生過多影響;
- 后續可以很方便的對該特性進行改造,比如可以把這個文件夾移到一個單獨的 module 中進行模塊化相關的改造;
- 這方式在大型項目中的優勢會更加明顯;
速記手冊
整理了一些關鍵知識點,可以保存圖片定期回顧。
官方材料
文章中的內容基本上都是參考官方文檔以及 Youtube 上的 mad – arch 系列。都看到這里了建議您到官方文檔中的 pathawy 地址中獲取下現代 Android 應用架構徽章,只要閱讀完下面的文檔以及完成對應測試即可。
- Youtube
https://www.youtube.com/watch?v=TPWmfJq16rA&list=PLWz5rJ2EKKc8GZWCbUm3tBXKeqIi3rcVX&ab_channel=AndroidDevelopers
- pathawy
https://developer.android.google.cn/courses/pathways/android-architecture
- ui-layer
https://developer.android.google.cn/jetpack/guide/ui-layer
- ui-layer/events
https://developer.android.google.cn/jetpack/guide/ui-layer/events
- domain-layer
https://developer.android.google.cn/jetpack/guide/domain-layer
- data-layer
https://developer.android.google.cn/jetpack/guide/data-layer
- youtube playlist
https://www.youtube.com/playlist?list=PLWz5rJ2EKKc8GZWCbUm3tBXKeqIi3rcVX
- Modern Android App Architecture quiz
https://developer.android.google.cn/courses/quizzes/android-architecture/architecture-layers?continue=https://developer.android.com/courses/pathways/android-architecture#quiz-/courses/quizzes/android-architecture/architecture-layers
最后
今年的 Google I/O 發布了一個最新的官方示例 Now in Android,這個示例的完整度比之前的 JetNews、Sunflower 要高,后面也將基于這個倉庫做進一步的說明解析,從一個完整項目的角度來看 Android 新推出的架構指南。
- Now in Android
https://github.com/android/nowinandroid
這里也分享一些珍藏資源,從面試簡歷模板到大廠面經匯總,從大廠內部技術資料到互聯網高薪必讀書單,以及Android面試核心知識點(844頁)和Android面試題合集2022年最新版(354頁)等等,這些資料整理給大家,希望踩過的坑不要再踩,遭遇的技術瓶頸一次性消滅。
如果需要的話,可以順手幫我點贊評論一下,直接私信我【筆記】免費領取!
部分內容展示如下
01.Android必備底層技術:
- Java序列化:Serializable原理、Parcelable接口原理、Json、XML
- 注解、泛型與反射:自定義注解、注解的使用、泛型擦除機制、泛型邊界、Java方法與Arm指令、Method反射源碼、invoke方法執行原理
- 虛擬機:JVM垃圾回收器機制、JVM內存分配策略、Android虛擬機與JVM底層區別、虛擬機底層Odex本地指令緩存機制、虛擬機如何分別加載class與object、虛擬機類加載模型
- 并發:Java線程本質講解、線程原理、線程通信、UnSafe類、線程池
- 編譯時技術:OOP面向切面之AspectJ、字節碼手術刀JavaSSit實戰、字節碼插樁技術(ASM)實戰
- 動態代理:動態代理實現原理、動態代理在虛擬機中運行時動態拼接Class字節碼分析、ProxyGenerator生成字節碼流程
- 高級數據結構與算法:HashMap源碼、ArrayList源碼、排序算法
- Java IO:Java IO體系、IO文件操作
02.Framework:
- Binder:Linux內存基礎、Binder四層源碼分析、Binder機制、Binder進程通信原理
- Handler:Loop消息泵機制、Message解析
- Zygote:init進程與Zygote進程、Zygote啟動流程、Socket通信模式、APP啟動過程
- AMS:ActivityThread源碼分析、AMS與ActivityThread通信原理、Activity啟動機制
- PMS:PMS源碼、APK安裝過程分析、PMS對安裝包的解析原理
- WMS:PhoneWindow實例化流程、DecorView創建過程、ViewRootImpl渲染機制
03.Android常用組件:
- Activty:Activity管理棧與Activity的啟動模式、Activity生命周期源碼分析
- Fragment:Fragment生命周期深入詳解、Fragment事務管理機制詳解、性能優化相關方案
- Service:Service啟動模式分析、Service管理與通信方案、Service生命周期底層詳解
04.高級UI:
- UI繪制原理:setContentView()方法下到底做了什么、AppCompatActivity與Activity的區別、UI測量、布局、繪制的底層執行流程
- 插件換膚:LayoutInflater加載布局分析、Android資源的加載機制、Resource與AssetManager
- 事件分發機制原理:事件執行U形鏈與L形鏈、事件攔截原理
- 屬性動畫:VSYNC刷新機制、ObjectAnimator與ValueAnimator源碼講解、Android屬性動畫:插值器與估值器
- RecycleView:布局管理器LayoutManager詳解、回收池設計思想、適配器模式原理
- 高階貝塞爾曲線
05.Jetpack:
- Lifecycle:Lifecycle源碼、Lifecycle高階應用
- ViewModel:ViewModel源碼、ViewModel應用技巧
- LiveData:LiveData源碼
- Navigation:Navigation源碼
- Room:Room源碼、Room LiveData監聽數據庫數據變更刷新頁面原理
- WorkManager內核
- Pagging原理
- DataBinding:單向綁定、雙向綁定、如何與RecyclerView的配合使用、底層原理
06.性能優化:
- 啟動優化:系統啟動原理、Trace工具分析啟動卡頓、類重排機制、資源文件重排機制
- 內存優化
- UI渲染優化:UI層級規范及對UI加載的影響、UI卡頓原因及修復、UI繪制、布局、測量原因以及處理方案
- 卡頓優化:造成卡頓的原因分析、內存抖動與GC回收、回收算法
- 耗電優化
- 崩潰優化:項目崩潰異常捕獲、優雅的異常處理方案、如何避免異常彈框
- 安全優化:APP加固實現(防反編譯,dex加固)、https防抓包機制(數據傳輸加載,客戶端服務器端雙向加密校驗)
- 網絡優化:serializable原理、parcelable接口原理、http與https原理詳解、protbuffer網絡IO詳解、gzip壓縮方案
- 大圖加載優化:Glide巨圖加載機制原理分析、大圖多級緩存實現方案
- 多線程并發優化
- 儲存優化:Android文件系統-sdcard與內存存儲、Shared Preference原理、MMAP內存映射
- 安裝包優化:shrinkResources去除無用資源、合理設置多語言、webp實現圖片瘦身、合理配置armable-v7的so庫、Lint檢查工具實踐
如果需要的話,可以順手幫我點贊評論一下,直接私信我【筆記】免費領取!
07.音視頻:
- C/C :數據類型、數組、內存布局、指針、函數、預處理器、結構體、共用體、容器、類型轉換、異常、文件流操作、線程
- H.265/H.265:音視頻格式封裝原理、編碼原理、視頻流H264的組裝原理切片NAL單元、視頻流H264碼流分析、切片與宏快,運動矢量、信源編碼器、高頻濾波、幀間拆分與幀內預測、CTU,PU TU編碼結構、DSP芯片解碼流程、MediaPlayer與DSP芯片交互機制、投屏架構、MediaProjection與MeidiaCodec交互機制、H265碼流交換
- MediaCodec:dsp芯片、編解碼器的生命周期、解碼器中輸入隊列與解析隊列設計思想、MediaCodec中平緩解碼解析、MediaExtractor 多路復用、MediaMuxer合成器、MediaFormat格式
- 音視頻剪輯:視頻剪輯、音頻剪輯、音頻合成、音譜顯示、視頻倒放
- 音視頻直播:硬編碼、軟編碼、native實現rtmp推流、攝像頭預覽幀編碼NV21轉YUV、視頻畫面封裝拼接Packet包、音頻流數據拼接Packet包、RtmpDump實時同步發送音視頻數據、MediaProjection、Medicodec編碼H264碼流、rtmp推流
- OpenGL與音視頻解碼:OpenGL繪制流程、矩陣、Opencv詳解、人臉識別效果實現
- OpenGL特效:CPU與GPU運行機制詳解、世界坐標,布局坐標,與FBO坐標系、圖像鏡像與旋轉處理、人臉定位與關鍵點定位、大眼效果、貼紙效果、美顏效果
- FFmpeg萬能播放器:FFmpeg結構體、聲音播放原理、Surface的渲染、像素繪制原理與對齊機制、音視頻同步原理、視頻播放器整體架構
- Webrtc音視頻通話:WebRtc服務端環境搭建與Webrtc編譯、1v1視頻通話實現方案、群聊視頻通話實現思路、多對多視頻會議實現、1V1音視頻通話實現
08.開源框架原理:
- Okhttp
- Retrofit
- RxJava
- Glide
- Hilt
- Dagger2
- EventBus
- 組件化、插件化、熱修復等
09.Gradle:
- Groovy語法
- Gradle Android插件配置
- Gradle實踐等
10.kotlin:
- Kotlin語法
- 擴展使用
- 進階使用
- 實踐等
11.Flutter:
- Dart語法
- UI
- 進階使用
- 優化
- 實踐等
12.鴻蒙:
- Ability組件
- 分布式任務
- 事件總線
- 鴻蒙線程
- UI自定義控件等
如果需要的話,可以順手幫我點贊評論一下,直接私信我【筆記】免費領取!
Android路漫漫,共勉!
版權聲明:本文內容由互聯網用戶自發貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如發現本站有涉嫌抄襲侵權/違法違規的內容, 請發送郵件至 舉報,一經查實,本站將立刻刪除。