Android 架構組件(一)

轉載:https://www.yuque.com/liangfei/programming/hr8o47

Google 爲了幫助 Android 開發者更快更好地開發 App,推出了一系列組件,這些組件被打包成了一個整體,稱作  Android Jetpack,它包含的組件如下圖所示:


Jetpack.png

 

老的 support 包被整合進了 Jetpack,例如上圖 Foundation 模塊的 AppCompat,整合進去之後,包名做了一下修改,全部以 androidx 開頭。Android Studio 提供的遷移工具(Refactor > Migrate to AndroidX)可以將源碼中的舊包名替換成新的,但是如果 Maven 依賴的產物還未遷移到 AndroidX 的話,還需要配置一個工具—— Jetifier,只需要在 build.gradle 中加上兩行配置即可:

 

android.useAndroidX=true
android.enableJetifier=true

 

Jetfier 會在編譯階段直接修改依賴產物的字節碼,簡單粗暴。

 

架構大圖

Jetpack 不屬於 Android Framework,不是 Android 開發的必需品,它只是應用層開發的一種輔助手段,幫我們解決了一些常見問題,比如版本兼容、API 易用性、生命週期管理等。其中 Architecture 部分的組件(Android Architecture Components,以下簡稱 AAC)組合起來形成了一套完整的架構解決方案,在沒有更好的方案被髮明出來之前,我們姑且把 AAC 當做 Android 架構領域的最佳實踐,它的出現一定程度上避免了很多不必要的輪子。

 

官方給出的架構指導非常明確地表達出了每個架構組件的位置:

 

image.png

 

這張圖背後隱含了三大設計思想:

  • 關注點分離(SOC / Separation Of Concerns)
  • 數據驅動 UI(Reactive
  • 唯一真相源(SSOC / Single Source Of Truth)

 

SOC 具體到工程實踐中就是分層合理,單層的職責越明確,對上下游的依賴越清晰就意味着它的結構更穩定,也

更可測(testable)。一個 App 從全局來看,可以劃分爲三部分:首先是 UI Controller 層,包含 Activity 和 Fragment;其次是 ViewModel 層,既可以做 MVVM 的 VM、MVP 的 P,也可以做 UI 的數據適配,這一層可以實現數據驅動 UI;最後是 Repository 層,它作爲 SSOC,是一個 Facade 模式,對上層屏蔽了數據的來源,可以來自 local,也是來自 remote,數據持久化策略向上透明。

 

一張架構藍圖,三大設計原則,接下來深入細節,看看組件之間如何配合才能實現這個架構。

 

Lifecycle

與 React/Vue 或者 iOS 相比,Android 的生命週期都比較複雜,如果要監聽生命週期,一般情況下只能覆寫 Activity / Fragment 的回調方法(onCreate、onResume、onPause、onDestroy 等),樣板代碼少不了,可維護性也變差。

 

如果要對生命週期進行簡化,可以抽象成一個圖,表示狀態,表示事件:

 

image.png

 

Lifecycle 負責處理這些點(states)和線(events),Activity / Fragment 是 LifecycleOwner,監聽者則是 LifecycleObserver,一個非常清晰的觀察者模式。

 

class MyObserver : LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun connectListener() {
        ...
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun disconnectListener() {
        ...
    }
}

 

如果我們的組件需要強綁定聲明週期,那麼只需要藉助 Lifecycle 去監聽生命週期的狀態和事件即可,再也不用覆寫各種回調方法了。下面將要講到的 LiveData 和 ViewModel 都是 Lifecycle-Aware Components,它們都用到了 Lifecycle。

 

Android 生命週期管理不當帶來的最大問題就是內存泄露,舉一個我們經常遇到的場景:一個異步任務(比如網絡請求)持有了 UI 元素的引用,只要任務沒有執行完,所有與這個 UI 元素有強引用關係的元素都沒法被 GC,如果這樣的場景多發生幾次,很可能會引起 OOM。

 

爲了異步對象引用的問題,最早我們使用 AsyncTask,任務執行在 worker thread,執行結果在主線程上發起回調。AsyncTask 的致命缺點是不支持流式數據(stream),而且回調嵌套太深(callback hell),與軟件質量衡量指標之一的 maintainable 背道而馳,不好用自然就會慢慢被淘汰。

 

後來我們開始使用 RxJava,響應式編程,聲明式寫法,再借助 retrolambda 這種 backport,即使當年 Android 只支持到 JDK7,我們依然可以利用各種 operator 寫出非常簡潔的代碼,“filter map 我閉~着眼”。RxJava 不但完美解決了線程調度的問題,還爲我們提供了 OO 之外的抽象——作用在流上的 lambda,基於函數的抽象。但是,即便完美如斯,生命週期的問題依然無法迴避,因爲 Java 天生的侷限性,一個 lambda 無論僞造地再像高階函數,它本質上還是一個匿名內部類,這個匿名內部類依然持有對 outer class 實例的引用。於是我們必須通過 CompositeDisposable 來管理訂閱關係,發起異步操作時記錄訂閱,離開頁面時取消訂閱,仍然需要覆寫 onDestory 或者 onPause 。

 

如果我們以 Repository 層爲界把架構藍圖分爲上下兩部分的話,上面的部分是數據展示,下面的部分是數據獲取,數據獲取部分因爲要請求 Remote 數據,必然會依賴到線程調度,而數據展示必然運行在 UI 線程,與生命週期強相關,這個時候就需要 LiveData 登場了。

 

LiveData

LiveData 也是一個觀察者模型,但是它是一個與 Lifecycle 綁定了的 Subject,也就是說,只有當 UI 組件處於 ACTIVE 狀態時,它的 Observer 才能收到消息,否則會自動切斷訂閱關係,不用再像 RxJava 那樣通過 CompositeDisposable 來手動處理。

 

LiveData 的數據類似 EventBus 的 sticky event,不會被消費掉,只要有數據,它的 observer 就會收到通知。如果我們要把 LiveData 用作事件總線,還需要做一些定製,Github 上搜 SingleLiveEvent 可以找到源碼實現。

 

我們沒法直接修改 LiveData 的 value,因爲它是不可變的(immutable),可變(mutable)版本是 MutableLiveData,通過調用 setValue(主線程)或 postValue(非主線程)可以修改它的 value。如果我們對外暴露一個 LiveData,但是不希望外部可以改變它的值,可以用如下技巧實現:

 

private val _waveCode = MutableLiveData<String>()
val waveCode: LiveData<String> = _waveCode

 

內部用 MutableLiveData ,可以修改值,對外暴露成 LiveData 類型,只能獲取值,不能修改值。

 

LiveData 有一個實現了中介者模式的子類 —— MediatorLiveData,它可以把多個 LiveData 整合成一個,只要任何一個 LiveData 有數據變化,它的觀察者就會收到消息:

 

 val liveData1 = ...
 val liveData2 = ...

 val liveDataMerger = MediatorLiveData<>();
 liveDataMerger.addSource(liveData1) { value -> liveDataMerger.setValue(value))
 liveDataMerger.addSource(liveData2) { value -> liveDataMerger.setValue(value))

 

綜上,我們彙總一下 LiveData 的使用場景:

  • LiveData - immutable 版本
  • MutableLiveData - mutable 版本
  • MediatorLiveData - 可彙總多個數據源
  • SingleLiveEvent - 事件總線

 

LiveData 只存儲最新的數據,雖然用法類似 RxJava2 的 Flowable,但是它不支持背壓(backpressure),所以不是一個流(stream),利用 LiveDataReactiveStreams 我們可以實現 Flowable 和 LiveData 的互換。

 

如果把異步獲取到的數據封裝成 Flowable,通過 toLiveData 方法轉換成 LiveData,既利用了 RxJava 的線程模型,還消除了 Flowable 與 UI Controller 生命週期的耦合關係,藉助 Data Binding 再把 LiveData 綁定到 xml UI 元素上,數據驅動 UI,妥妥的響應式。於是一幅如下模樣的數據流向圖就被勾勒了出來:


LiveData.png

 

圖中右上角的 Local Data 是 AAC 提供的另一個強大武器 —— ORM 框架 Room。

 

Room

數據庫作爲數據持久層,其重要性不言而喻,當設備處於離線狀態時,數據庫可用於緩存數據;當多個 App 需要共享數據時,數據庫可以作爲數據源,但是基於原生 API 徒手寫 CRUD 實在是痛苦,雖然 Github 上出現了不少 ORM 框架,但是它們的易用性也不敢讓人恭維,直到 Room 出來之後,Android 程序員終於可以像 mybatis 那樣輕鬆地操縱數據庫了。

 

Room 是 SQLite 之上的應用抽象層,而 SQLite 是一個位於 Android Framework 層的內存型數據庫。雖然 Realm 也是一個優秀的數據庫,但是它並沒有內置於 Android 系統,所會增大 apk 的體積,使用 Room 則沒有這方面煩惱。

 

Room 的結構抽象得非常簡單,數據對象(表名 + 字段)用 @Entity 註解來定義,數據訪問用 @Dao 來註解,db 本身則用 @Database 來定義,如果要支持複雜類型,可以定義 @TypeConverters,然後在編譯階段,apt 會根據這些註解生成代碼。Room 與 App 其他部分的交互如下圖所示:

 

image.png

 

Entity 是一個數據實體,表示一條記錄,它的用法如下:

 

@Entity(tableName = "actors")
data class Actor(
        @PrimaryKey @ColumnInfo(name = "id")
        val actorId: String,
        val name: String,
        val birthday: Date?,
        val pictureUrl: String
)

 

Actor 是一個用 @Entity 註解的 data class,它會生成一個名字是 actors 的表,注意到有一個字段是 @Date? ,但是 SQLite 本身不支持這種複雜類型(complex type),所以我們還需要寫一個可以轉換成基礎類型的轉換器:

 

class Converters {
    @TypeConverter
    fun timestampToDate(value: Long?) = value?.let { Date(it) }

    @TypeConverter
    fun dateToTimestamp(date: Date?) = date?.time
}

 

轉換器通過 @TypeConverters 可作用於 class、field、method、parameter,分別代表不同的作用域。比如作用在 @Database 類的上,那麼它的作用域就是 db 中出現的所有 @Dao 和 @Entity

 

@Database(entities = [Actor::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun actorDao(): ActorDao
}

 

代碼出現的 ActorDao 定義了 CRUD 操作。用 @Dao 來註解,它既可以是一個接口,也可以是抽象類,用法如下:

 

@Dao
interface ActorDao {
    @Query("SELECT * FROM actors WHERE id = :actorId")
    fun getActor(actorId: String): LiveData<Actor>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(actors: List<Actor>)
}

 

@Query 中的 SQL 語句可以直接引用方法參數,而且它的返回值可以是 LiveData 類型,也支持 Flowable 類型,也就是說,Room 原生支持響應式,這是對數據驅動最有利的支持,也是 Room 區別於其他 ORM 框架的顯著特徵。

 

至此,我們可以確定,無論數據來自 Remote 還是來自本地 DB,架構藍圖中的 Repository 對 ViewModel 提供的數據可以永遠是 LiveData 類型,接下來我們看一下 ViewModel 的妙用。

 

ViewModel

ViewModel 是一個多面手,因爲它的生命週期比較長,可以跨越因爲配置變動(configuration changed,比如屏幕翻轉)引起的 Activity 重建,因此 ViewModel 不能持有對 Activity / Fragment 的引用。

image.png

 

如果 ViewModel 中要用到 context 怎麼辦呢?沒關係,框架提供了一個 ViewModel 的子類 AndroidViewModel ,它在構造時需要傳入 Application 實例。

 

既然 ViewModel 與 UI Controller 無關,當然可以用作 MVP 的 Presenter 層提供 LiveData 給 View 層,因爲 LiveData 綁定了 Lifecycle,所以不存在內存泄露的問題。除此之外,ViewModel 也可以用做 MVVM 模式的 VM 層,利用 Data Binding 直接把 ViewModel 的 LiveData 屬性綁定到 xml 元素上,xml 中聲明式的寫法避免了很多樣板代碼,數據驅動 UI 的最後一步,我們只需要關注數據的變化即可,UI 的狀態會自動發生變化。

 

ViewModel 配合 Data Binding 的用法與 React 非常相似,ViewModel 實例相當於 state,xml 文件就好比 render 函數,只要 state 數據發生變化,render 就會重新渲染 UI,但是 data binding 還有更強大的一點,它支持雙向綁定。舉個例子,UI 需要展示一個評論框,允許展示評論,也允許用戶修改,那麼我們可以直接把 EditText 雙向綁定到一個 LiveData 之上,只要用戶有輸入,我們就可以收到通知,完全不需要通過 Kotlin/Java 來操控 UI:

 

<TextInputEditText
    android:text="@={viewModel.commentText}" />

 

注意,如果要在 xml 中使用 LiveData,需要把 lifecycle owner 賦給 binding:

 

val binding: MainBinding = DataBindingUtil.setContentView(this, R.layout.main)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)

 

因爲 ViewModel 拿到的數據是 Repository 給的,可能不適用於 UI 元素,所以 ViewModel 還承擔了數據適配的工作,有時候我們需要彙總 repository 的多個返回值一次性給到 UI,那麼就可以使用 LiveData 的“操作符” Transformations.switchMap,用法可以認爲等同於 Rx 的 flatMap;如果只想對 LiveData 的 value 做一些映射,可以使用 Transformations.map,目前 Transformations 只有這兩個操作符,因爲不管 Kotlin 還是 Java8,都提供了很多聲明式的操作符,對流的支持都比較友好,而 LiveData 本身不是一個流,所以這兩個操作符足矣。

 

除了數據適配之外,ViewModel 還有一個強大的用法 —— Fragment 之間共享數據,這樣 ViewModel 又扮演了 FLUX 模式中的 store 這一角色,是多個頁面(fragment)之間唯一的數據出口。


Redux.png

 

 

ViewModel 的用法也非常簡單,通過 ViewModelProviders.of 可以獲取 ViewModel 實例:

 

val viewModel = ViewModelProviders.of(requireActivity(), factory)
        .get(ActorViewModel::class.java)

 

一通操作猛如虎之後,UI controller 層變得薄如蟬翼,它只做了一件事情,把數據從左手(ViewModel)倒給了右手(使用了 Data Binding 的 xml)。

 

如果把 ViewModel 作爲 SSOC(唯一真相源),多個 Fragment 之間共享數據,再利用 SingleLiveEvent 做總線,一個 Activity 配多個 Fragment 的寫法就避免了 Activity 之間通過 Intent 傳遞數據的繁瑣。但是 Fragment 的堆棧管理一直是一個讓人頭疼的問題,AAC 的 Navigation 不但完美解決了這個問題,而且還提供可視化的路由,只需拖拽一下就能生成類型安全的跳轉邏輯。

 

Navigation 用一個(graph)來表示頁面間的路由關係,圖的節點(node)表示頁面,邊(edge)表示跳轉關係。例如下圖 8 個頁面的跳轉關係,一目瞭然:

image.png

 

頁面與頁面之間的連線叫 action,它可以配置進離場動畫(Animations),也可以配置出棧行爲(Pop Behavior),還支持 Single Top 的啓動選項(Launch Options)。進離場動畫和啓動選項很好理解,出棧行爲是一個比較強大的功能,action 箭頭所指的方向表示目標頁面入棧,箭頭的反方向則表示目標頁面出棧,而出棧的行爲在 Navigation 編輯器中完全可控,我們可以指定要出棧到哪個頁面,甚至可以指定目標頁面是否也需要出棧:

 

image.png

 

針對頁面節點,還可以定義它要接收的參數(arguments),支持默認值,從此 Fragment 之間的參數傳遞變得非常直觀,非常安全。

 

看一下具體用法,首先在跳轉發起頁面,通過 apt 生成的跳轉函數傳入參數:

val actorId = getSelectedActorId()
val direction = ActorListFragmentDirections.showDetail(actorId)
findNavController().navigate(direction)

 

注意:如果使用 LiveData 或其他來觸發跳轉邏輯,navigate 會多次執行,從而導致 crash,所以要麼在 navigate 執行結束之後移除掉 observer,或者通過 AtomicBoolean 來控制只執行一次。

 

然後利用目標頁面生成的 *Args 獲取參數:

private val args: ActorDetailFragmentArgs by navArgs()

 

這裏的 navArgs 是一個擴展函數,利用了 Kotlin 的 ReadWriteProperty。

 

幾行代碼就搞定了頁面之間的跳轉,而且還是可視化!從沒有想過 Android 的頁面跳轉竟會變得如何簡單,但是 Navigation 的方案並不是原創,iOS 的 Storyboard 很早就支持拖拽生成路由。當年 Android 推出 ConstraintLayout 之時,我們都認爲是參考了 Storyboard 的頁面拖拽,現在再配上 Navigation,從頁面到跳轉,一個完整的拖拽鏈路就形成了。平臺雖然有差異化,但是使用場景一致的前提下,解決方案也就殊途同歸了。

 

瞭解完了與生命週期有關的組件,接下來我們來看細節。

 

Paging

UI 沒有辦法一次性展示所有的數據,端上的系統資源(電量、內存)也有限制,不可能把所有數據都加載到內存中;而且大批量請求數據不但浪費帶寬,在某些網絡情況(弱網、慢網)下還會導致請求失敗,所以分頁是很多情景下的剛需。Github 上有各式各樣的解決方案,這一次,Google 直接推出了官方的分頁組件——Paging。

 

Paging 將分頁邏輯拆解爲三部分:

  • 數據源 DataSource
  • 數據塊 PagedList 
  • 數據展示 PagedListAdapter

 

DataSource 的數據來源於後端服務或者本地數據庫,並且用三個子類來表示三種分頁模式:

  • PageKeyedDataSource - 單頁數據以 page key 爲標識,例如當前頁的 Response 中包含了下一頁的 url,這個 url 就是 page key。
  • ItemKeyedDataSource - 單頁數據以 item key 爲標識,比如下一頁的請求要帶當前頁最後一個 item 的 id,這個 itemId 就是 item key。
  • PositionalDataSource - 單頁數據以位置爲標識,這種模式比較常見,Room 只支持這一種,因爲數據庫查詢以 OFFSET 和 LIMIT 做分頁。

 

PageKeyedDataSource 和 ItemKeyedDataSource 適用於內存型數據,比如直接從後端獲取後需要展示的數據。PositionalDataSource 適用於本地 Room 數據或者使用 Room 做緩存的 Cache 數據。

 

數據流向的關係圖如下所示:

 

Paging.png

 

LivePagedListBuilder 利用 DataSource.Factory 和 PageList.Config 創建 LiveData,UI Controller 拿到數據之後交給 PagedListAdapter 展示到 RecyclerView。

 

上圖表達了數據的流向,如果從 UI 層往回看,頁面展示的數據存儲在 PagedList 中,PagedList 只是 DataSource 的數據塊(chunk),當 PagedList 需要更多數據時,DataSource 就會給更多,當 DataSource 一無所有時便會觸發 BoundaryCallback 獲取更多數據,直到數據全部展示完畢。

 

LivePagedListBuilder 會將 PagedList 包裝成 LiveData<PagedList> 給到下游,它在整個數據交互鏈路中的位置如下圖所示:

Paging sequence.png

Repository 拿到 Dao 的 DataSource.Factory 之後,調用它的 toLiveData 方法並傳入 PagedList.Config,然後生成一個分頁的 LiveData<PagedList> 交給 ViewModel 層。

 

Paging 加上生命週期相關的架構組件解決了數據存儲、數據流轉和數據展示的問題。除此之外,AAC 還包括一個強大的異步任務執行器 WorkManager,它解決了任務執行的可靠性,無論 App 退出還是設備重啓,交給 WorkerManager 的任務都會被執行。

 

WorkManager

WorkManager 雖然解決了任務執行可靠性的問題,但是它無法精確控制任務的執行時間,因爲 WorkManager 要根據 OS 資源來選擇執行任務。Android 自身提供了很多方案來解決後臺任務執行的問題,可以根據下圖的決策路徑選擇不同的組件:

background.png

WorkManager 整體上可分爲四部分:任務類型、任務構建、任務監控和任務控制。

 

一、任務類型,WorkManager 提供了一次性任務週期性任務兩種任務類型:

  • OneTimeWorkRequest —— 一次性任務
  • PeriodicTimeWorkRequest —— 週期性任務

 

二、任務構建,一是執行條件,二是執行順序。

  • Constraints —— 通過 Constraints.Builder 構建任務執行的條件(網絡類型、電量、設備空間等)
  • WorkContinuation —— 可以指定任務的執行順序,例如可以按照 PERT 圖的順序執行任務:

 

WorkContinuation.png

 

三、任務監控,通過回調來獲知任務的當前狀態:

State.png

 

四、任務控制,包括加入隊列,取消任務,其中 UniqueWork 提供了多種加入隊列的策略(REPLACE、KEEP、APPEND):

  • cancelWorkById(UUID) —— 通過 ID 取消單個任務
  • cancelAllWorkByTag(String) —— 通過 Tag 取消所有任務
  • cancelUniqueWork(String) —— 通過名字取消唯一任務

 

除此之外,WorkerManager 還提供了四種不同線程模型的 Worker:

  • Worker —— 基於默認後臺線程
  • CoroutineWorker —— 基於 Kotlin 的協程
  • RxWorker —— 基於 RxJava2
  • ListenableWorker —— 基於回調的異步

 

總結

Google 官方架構組件 AAC 爲我們提供了太多通用問題的解決方案,使用場景包括數據持久化、異步任務調度、生命週期管理,UI 分頁、UI 導航,當然還有強大的 MVVM 框架 Data Binding,這些架構組件不但使代碼變得清晰易讀,而且獨立於 Android SDK 向下兼容,AAC 使我們更加聚焦產品,專注於解決問題,而不是花太多的時間重複造輪子

 

參考資料

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