Android多線程技術選型最全指南 [復制鏈接]

2019-7-30 09:48
Newpaper 閱讀:588 評論:0 贊:0
Tag:  多線程

維度的Trade Off

今天我想先說一個英文單詞,叫Trade Off。 中文翻譯過來可以說叫權衡,妥協,但是這么干巴巴的翻譯可能不能體現這個詞的牛逼之處,我來舉個例子。比如迪麗熱巴和謝娜同時追求我,雖然迪麗熱巴顏值更高,但是考慮到謝娜在湖南臺的地位以及和她在一起之后能給我帶來的曝光度,我選擇了謝娜。。。。(以上純屬段子)

Android多線程技術選型最全指南

Anyway。。。這就是Trade Off,一個很艱難的選擇,但是最后人都是趨于自己的利益最大化做出最后的決定。 Trade Off這個詞貫穿了軟件開發的所有流程,在多線程的選擇下面也是有一樣的體現。

谷歌官方在18年的IO大會上放了這么一張圖

我先來翻譯翻譯這張圖。

橫軸從左往右分別是Best-Effort(可以理解為盡力而為)還有Guaranteed Execution(保證執行). 豎軸從上往下分別是Exact Timing(準確的時間點)還有Deferrable(可以被延遲). 這張圖分別從在多線程下執行的代碼的可執行性和執行時間來把框架分成了四個維度。其中我想先說說個人的理解: 對于安卓里面的里面的任何代碼,都逃不開生命周期這個話題。因為安卓的四大組件有兩個都是有生命周期的,而且對于用戶來說,可見的Activity或者Fragment才是他們最關心app的部分。所以一段代碼,在保證沒有內存泄漏的情況下,能不能在異步框架下執行完畢,就得取決于代碼所在載體(Activity/Fragment)的生命周期了。比如上一期我們說到的RxJava的例子:

 @Override
protected void onDestroy() {
super.onDestroy();
//onDestroy 里面對RxJava stream進行unsubscribe,防止內存泄漏
subscription.unsubscribe();
}

這段代碼有可能會阻止我們在Observable里面的API進行調用。

那么在安卓的生命周期的背景下,這段代碼就是Best Effort,盡力而為了。能跑就跑,要是activity沒了,那就拉倒。。。

Android多線程技術選型最全指南

所以把以上例子中的代碼換成圖中的ThreadPool想必你就理解了。

那么Guaranteed Execution呢? 很顯然在圖中是用Foreground service來做。不像Activity或者Fragment,Service雖然也有生命周期,但是他的生命周期不像前兩者是被用戶操控。 Service的生命周期可以由開發者來決定,因此我們可以使用Foreground service + ThreadPool,來保證代碼一定可以被執行。用Foreground Service是因為Android在Oreo之后修改了Service的優先級,在app 進入后臺idle超過一分鐘之后會自動殺死任何后臺Service。但是,使用Foreground Service,要求開發者一定要開啟一個Notification。

 @Override
public void onCreate() {
super.onCreate();
startForeground(1, notification);
Log.d(TAG_FOREGROUND_SERVICE, "My foreground service onCreate().");
}

這下好了,雖然保證程序正常運行了,我們的UX卻變了,你還得和設計獅們苦口婆心的解釋,這都是安卓谷歌的鍋!我也不想有個突兀的圖標出現在狀態欄里。。。我還記得我去年在修改我們產品下載音樂的Service時候,為了讓Service不被銷毀把Notification變成Foreground service 的notification,我們的產品經理還跑來問我為啥這個notification不能劃掉。。。。也是花了很長時間來給產品經理科普。

你看這就是Trade Off,從盡力而為到想保證代碼必須運行。中間有這么一個需要權衡的地方。

那么咱又開始琢磨了,既然Foreground Service這么蛋疼,能不能要一個可以保證執行,但是不改變咱app的UX的框架呢。

當當當當!WorkManager閃亮登場。

Android多線程技術選型最全指南

說起這個框架就屌了。使用它可以輕松的實現異步任務的調度,運行。當然僅僅是普通的執行異步任務好像沒那么吸引人,畢竟很多其他的優秀異步框架也可以實現。我們看看官方的解釋: The WorkManager API makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or device restarts.

劃重點,even if the app exits or device restarts,意思是即使app退出或者重啟,也可以保證你的異步任務完整的執行完畢。這個就完美的解決了我們用Foreground Service或者ThreadPool的問題,它既可以保證任務完整執行,也不需要以為啟動前臺服務而導致需要UX的改變!

我這里就不詳細解釋WorkManager的實現細節和源碼了。我們直接以上次的youtube 取消訂閱的例子說話(這個例子用kotlin因為我懶得重新寫一個java版本的了。。。)! 我們先定義一個Worker:

class MakeSubscriptionWorker : Worker{
constructor(context: Context, parameterName: WorkerParameters):super(context,parameterName)
override fun doWork(): Result {
//unsubscribe 的API call在這里做
val api = API()
var response = api.unSubscribe()
if(response != null){
return Result.success(response)
}
else{
return Result.failure()
}
}
}

Worker里面其實就是執行我們的取消訂閱的API call。

接著監聽我們取消訂閱的成功與否

//1. 創建我們Worker的實例并且開始執行!
WorkManager.getInstance().enqueue(OneTimeWorkRequest.Builder(MakeSubscriptionWorker::class.java!!)
.addTag(MakeSubscriptionWorker::class.simpleName!!)
.build())
//2. 把API call的結果轉化成Jetpack里面的LiveData,并且開始監聽結果
WorkManager.getInstance().getWorkInfosByTagLiveData(MakeSubscriptionWorker::class.simpleName!!).observe(this,purchaseObservaer)
//3. 如果用戶退出了Activity,那么停止監聽結果
WorkManager.getInstance().getWorkInfosByTagLiveData(MakeSubscriptionWorker::class.simpleName!!).removeObserver(purchaseObservaer)

重點在第三步,雖然我們停止監聽了,但是不代表這個異步任務會取消。它還會繼續執行。

可是這和我們用線程池+非匿名內部類Runnable好像沒啥本質區別,畢竟在上面的例子里面,kotlin的內部class本身就是靜態的。不存在內存泄漏。

回到開頭我說的,WorkManager可以保證任務一定執行,即使你把app退出!

Android多線程技術選型最全指南

WorkManager會把你的任務序執行id和相關信息保存在一個數據庫中,在App重新打開之后會根據你在任務中設置的限制(比如有的任務限制必須在Wifi下執行,WorkManager提供這樣的API)來重新開啟你未完成任務。

也就是說,即使我們在點擊取消訂閱之后馬上把App強行關閉,下一次打開的時候WorkManager也可以重新啟動這個任務!!!

那。。。這么屌的功能為啥我們不馬上開始使用呢????

Android多線程技術選型最全指南

還記得我反復提到的Trade Off這個詞么,WorkManager也有它需要取舍的地方。

首先官方雖然重點說到了保證任務執行,但同時也提到了:

WorkManager is intended for tasks that are deferrable—that is, not required to run immediately

也就是說,WorkManager主要目的是為了那些允許/可以忍受延遲的異步任務而設計的。這個可以忍受延遲就很玩味了。有誰會想要無目的的延遲自己想要運行的異步任務的?這個問題的答案其實也是安卓用戶一直關心的電池續航。

安卓在經歷了初期的大開大方之后,開始越來越關心用戶體驗。既然App的開發者不遵守游戲規則(沒錯我說的就是那些不要臉的xx保活app),那么谷歌就自己制定規則,在新的操作系統中,谷歌進一步縮減后臺任務可以執行的條件。

具體限制

Android多線程技術選型最全指南

上圖中,簡潔的來說,當APP進入后臺之后,異步任務被限制的很死。那么作為谷歌自己研制的WorkManager,一個號稱app關掉之后還能重啟異步任務的這么吊炸天的框架當然也要遵循這個規則。

所以,所謂的延遲,并不是那么的嚇人,筆者親測,在App還在前臺的時候執行WorkManager,異步任務基本上還是馬上會進入調度執行的,但是當app進入后臺之后,WorkManager就會嘗試暫停任務。所以在我們上面的例子里面,WorkManager也是可以使用的。

但是!Trade Off又來了。雖然WorkManager和Activity的生命周期無關了,但是卻和整個App的前后臺狀態相關了。app的退出可以暫停WorkManager里面的任務,也就是說控制他能否執行的這個鑰匙,又從開發者手中跑到用戶的手里了。。。。

Android多線程技術選型最全指南

這說了大半章節的WorkManager,怎么又繞回來了呢。說了這么多,從ThreadPool到Foreground Service,再到WorkManager。我們好像每次都在解決一個問題之后又遇到了新的問題,好像沒有完美的方案。

沒錯,這些就是Trade Off,權衡,軟件開發本就沒有完美的答案,silver bullet只在殺吸血鬼的時候存在,軟件開發?不存在的。。。

復雜度的Trade Off

上面的篇幅我都在從谷歌官方的解釋,也就是從執行時間,和能否保證任務完整執行的維度來審視我們現有的解決方案。接下來我想從代碼的復雜角度來聊聊。

我在2015年開始接觸RxJava,剛開始學習RxJava的時候的確有點難懂,尤其是flatMap這個操作符消耗了我整整一周的時間去消化。但是在越來越熟悉之后,我就漸漸的愛上了RxJava。那個時候我就覺得,函數式編程的操作符實在太屌了,酷炫的操作符疊在一起,簡直是狂炫酷霸拽有沒有,加上團隊中懂RxJava的人不多,大家有問題都會找我,我的虛榮心也迅速膨脹到了月球。。。我記得當時我在重構一個app冷啟動的任務調度的代碼。

當時任務的依賴圖大概長這個樣子:

Android多線程技術選型最全指南

當我的隊友還在用LacthCoundown,焦頭爛額的時候。我輕松的用RxJava的mergeWith和ConcatMap解決了:

B
.mergeWith(C)
.concatMap(E)
.concatMap(F)
.mergeWith(A
.concatMap(D))

啥也不說了,屌就一個字! 這更加堅定了我RxJava就是世界上最好的異步任務框架的信念了。。。。

直到我從創業公司來到Amazon Music,從一個只有3個人的安卓團隊到了一個四個大組同時做一個產品的Org。我突然發現,推廣RxJava的時間成本,還有團隊學習的成本,已經不能和以前在創業公司同日而語了。剛開始的時候,每次看到隊友的code review我都喜歡插上一嘴:"you know , if we use RxJava here......", 直到團隊的Senior有一次和我問我:"Why RxJava is better?"的時候,我才意識到,我好像從來沒有系統性的總結過RxJava的優缺點,一時間有點語塞。我甚至發現, 有時候一些簡單的集合處理,用RxJava反而還顯得復雜了,況且RxJava的可讀性還是在基于團隊都熟悉的條件下,更不說因為學習成本導致產品迭代的減速了。那一刻,我仿佛丟了靈魂,我引以為傲的RxJava竟然被貶的一文不值!!!

Android多線程技術選型最全指南

不對啊,我們RxJava明明對異步任務的組合,連接有強大的支持!mergeWith,concatMap,這么牛逼的操作符,不就是使用RxJava最好的理由么!我這樣和Senior反擊到。。。

直到我看到了Coroutine。。。。

Coroutine的操作符也可以同樣的實現上面的例子,還更容易理解和閱讀。。。

Android多線程技術選型最全指南

如果想實現上面的四個異步任務同時執行,下面的偽代碼可以輕松實現。

//Dispatch code in Main thread , unless we swithc to antoehr
var job = GlobalScope.launch(Dispatchers.Main) {
//force task A B C D to run in IO thread
var A = async (Dispatchers.IO){//do something in IO thread pool }
var B = async (Dispatchers.IO){//do something in IO thread pool }
var C = async (Dispatchers.IO){//do something in IO thread pool }
var D = async (Dispatchers.IO){//do something in IO thread pool }
//join 4 tasks (similar to merge concept in RxJava),
A.await()
B.await()
C.await()
D.await()

這一刻我崩潰了,這個世界上竟然還有除了RxJava之外的框架可以做到組合連接。

也可能我高估了自己的預判能力,在學習WorkManager之后,我發現,WorkManager也有同樣的功能。。。 比如下面的串行執行異步任務

Android多線程技術選型最全指南

 WorkManager.getInstance()
.beginWith(OneTimeWorkRequest.Builder(MakeSubscriptionWorker::class.java!!).build())
.then(OneTimeWorkRequest.Builder(MakeSubscriptionWorker::class.java!!).build())
.then(OneTimeWorkRequest.Builder(MakeSubscriptionWorker::class.java!!).build())

RxJava -> Coroutine -> WorkManager

這三個框架對異步任務的連接,合并等等邏輯操作從強大到功能有所局限整齊的排列著,但同樣的,實現的復雜度也從高到底排列。

這又回到了我們開頭講的Trade Off。怎么樣從團隊,代碼復雜度和功能的強大與否直接做權衡。


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)

領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

时时彩改欢乐生肖