上一篇文章我介紹了一些使用安卓多線程框架們的一些誤區,那既然已經介紹了那麼多坑,這一篇我就來詳細說說一些方案。一樣的,這些總結下來的方案都是我本身我的的心得體會,本人水平有限,有什麼不對或者意見不一樣的歡迎你們討論或者吐槽。java
今天我想先說一個英文單詞,叫Trade Off。 中文翻譯過來能夠說叫權衡,妥協,可是這麼幹巴巴的翻譯可能不能體現這個詞的牛逼之處,我來舉個例子。好比迪麗熱巴和謝娜同時追求我,雖然迪麗熱巴顏值更高,可是考慮到謝娜在湖南臺的地位以及和她在一塊兒以後能給我帶來的曝光度,我選擇了謝娜。。。。(以上純屬段子)android
Anyway。。。這就是Trade Off,一個很艱難的選擇,可是最後人都是趨於本身的利益最大化作出最後的決定。 Trade Off這個詞貫穿了軟件開發的全部流程,在多線程的選擇下面也是有同樣的體現。數據庫
谷歌官方在18年的IO大會上放了這麼一張圖編程
我先來翻譯翻譯這張圖。api
橫軸從左往右分別是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進行調用。app
那麼在安卓的生命週期的背景下,這段代碼就是Best Effort
,盡力而爲了。能跑就跑,要是activity沒了,那就拉倒。。。框架
因此把以上例子中的代碼換成圖中的ThreadPool想必你就理解了。less
那麼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閃亮登場。
提及這個框架就屌了。使用它能夠輕鬆的實現異步任務的調度,運行。固然僅僅是普通的執行異步任務好像沒那麼吸引人,畢竟不少其餘的優秀異步框架也能夠實現。咱們看看官方的解釋: 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退出!
WorkManager會把你的任務序執行id和相關信息保存在一個數據庫中,在App從新打開以後會根據你在任務中設置的限制(好比有的任務限制必須在Wifi下執行,WorkManager提供這樣的API)來從新開啓你未完成任務。
也就是說,即便咱們在點擊取消訂閱以後立刻把App強行關閉,下一次打開的時候WorkManager也能夠從新啓動這個任務!!!
那。。。這麼屌的功能爲啥咱們不立刻開始使用呢????
還記得我反覆提到的Trade Off這個詞麼,WorkManager也有它須要取捨的地方。
首先官方雖然重點說到了保證任務執行,但同時也提到了:
WorkManager is intended for tasks that are deferrable—that is, not required to run immediately
也就是說,WorkManager主要目的是爲了那些容許/能夠忍受延遲的異步任務而設計的。這個能夠忍受延遲就很玩味了。有誰會想要無目的的延遲本身想要運行的異步任務的?這個問題的答案其實也是安卓用戶一直關心的電池續航。
安卓在經歷了初期的大開大方以後,開始愈來愈關心用戶體驗。既然App的開發者不遵照遊戲規則(沒錯我說的就是那些不要臉的xx保活app),那麼谷歌就本身制定規則,在新的操做系統中,谷歌進一步縮減後臺任務能夠執行的條件。
上圖中,簡潔的來講,當APP進入後臺以後,異步任務被限制的很死。那麼做爲谷歌本身研製的WorkManager,一個號稱app關掉以後還能重啓異步任務的這麼吊炸天的框架固然也要遵循這個規則。
因此,所謂的延遲,並非那麼的嚇人,筆者親測,在App還在前臺的時候執行WorkManager,異步任務基本上仍是立刻會進入調度執行的,可是當app進入後臺以後,WorkManager就會嘗試暫停任務。因此在咱們上面的例子裏面,WorkManager也是可使用的。
可是!Trade Off又來了。雖然WorkManager和Activity的生命週期無關了,可是卻和整個App的先後臺狀態相關了。app的退出能夠暫停WorkManager裏面的任務,也就是說控制他可否執行的這個鑰匙,又從開發者手中跑到用戶的手裏了。。。。
這說了大半章節的WorkManager,怎麼又繞回來了呢。說了這麼多,從ThreadPool到Foreground Service,再到WorkManager。咱們好像每次都在解決一個問題以後又遇到了新的問題,好像沒有完美的方案。
沒錯,這些就是Trade Off,權衡,軟件開發本就沒有完美的答案,silver bullet只在殺吸血鬼的時候存在,軟件開發?不存在的。。。
上面的篇幅我都在從谷歌官方的解釋,也就是從執行時間,和可否保證任務完整執行的維度來審視咱們現有的解決方案。接下來我想從代碼的複雜角度來聊聊。
我在2015年開始接觸RxJava,剛開始學習RxJava的時候的確有點難懂,尤爲是flatMap這個操做符消耗了我整整一週的時間去消化。可是在愈來愈熟悉以後,我就漸漸的愛上了RxJava。那個時候我就以爲,函數式編程的操做符實在太屌了,酷炫的操做符疊在一塊兒,簡直是狂炫酷霸拽有沒有,加上團隊中懂RxJava的人很少,你們有問題都會找我,個人虛榮心也迅速膨脹到了月球。。。我記得當時我在重構一個app冷啓動的任務調度的代碼。
當時任務的依賴圖大概長這個樣子:
當個人隊友還在用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居然被貶的一文不值!!!
不對啊,咱們RxJava明明對異步任務的組合,鏈接有強大的支持!mergeWith,concatMap,這麼牛逼的操做符,不就是使用RxJava最好的理由麼!我這樣和Senior反擊到。。。
直到我看到了Coroutine。。。。
Coroutine的操做符也能夠一樣的實現上面的例子,還更容易理解和閱讀。。。
若是想實現上面的四個異步任務同時執行,下面的僞代碼能夠輕鬆實現。
//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也有一樣的功能。。。 好比下面的串行執行異步任務
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。怎麼樣從團隊,代碼複雜度和功能的強大與否直接作權衡。
寫到最後我想稍微解釋一下,可能自己這兩篇文章有些許的標題黨,號稱最全的選型指南,這也是我想吸引眼球的一種方式,結果到最後也沒有給讀者一個結論到底用哪一個框架,若是有讀者由於這個緣由感受被欺騙了,那在此我想說一聲抱歉。不過我相信,在讀完這篇文章以後,你可能也會發現選型這個問題須要先了解框架自己使用的Trade Off。不能由於我喜歡,或者我以爲就輕易的作決定或者嘗試說服你的反對者或者老闆。若是個人文章可讓你稍微對多線程作技術選型的時候能多作一丟丟的思考,我想我也就達到了我寫這兩篇文章的初衷。