Try   HackMD

Android的background processing

App除了使用者看得到可互動的foreground部分之外,很多時候會用到background的運算,例如Google drive透過網路進行檔案的上傳下載、Line的即時訊息等等都是要在背景執行或是待命的。這篇會基本地介紹一下background processing的部分有哪些常用機制可以用,然後主要介紹其中兩個,第一個不外乎就是App四大component之一的Service,另一個是API 28出現的WorkManager。

官方建議的background processing方案

其實官方有文章來講解這個部分,這邊簡單重點整理一下,詳細內容可以去看官方的文章

官方把背景的工作分成三種

  1. Immediate
  2. Deferred
  3. Exact

區隔的依據是:
工作需要在使用者與app互動的時候就完成的 -> Immediate
工作需要在特定的時間被執行的 -> Exact
不屬於上述兩者的 -> Deffered

而以下就是這三種工作Android官方所建議的實作方案

  • Immediate
    用Kotlin coroutine或是Thread,如果要立刻且持續執行的話就用WorkManager,若有特殊需求可以用foreground service。
  • Deferred
    用WorkManager
  • Exact
    用AlarmManager

整個看完不難發現,WorkManager的功能是很強大的,而Service的某些功能似乎直接被WorkManager給取代掉了,尤其在是API 26對於background service特別不友善之後更是如此。

Service

看完Android官方的建議後,我們先從Service介紹起,畢竟是四大component之一,該給的尊重不能少。這個東西Android developers想必都不陌生,這邊快速重點整理一下,官方頁面在這邊

Serivce有三種:

  1. foreground service 這個是會常駐在notification bar的service,會被使用者直接看到,所以稱之為foreground。它跟在onPause()階段的Activity還有Fragment一樣都會讓該process變成visible process,也就是重要性第二高的process,基本上是不會被系統砍掉的,除非資源不夠時為了保全所有的foreground process(重要性第一的process)有可能被砍,但那情況很極端不太容易遇到。process的重要順序可以看這篇文章

  2. background service 這個就是在背景運作不會被使用者發現的service,在API 26之後Android針對它有做一些限制,讓他能力變得更弱一些,而Android官方也直接在background service的介紹中提到可以改用WorkManager來做背景作業的安排。

  3. bound service 這個是跟其他application component綁定的service,提供一個client-server的介面讓bound service跟它所綁定的component進行互動,也有橫跨process的IPC機制可以用。當所有跟這個bound service的component都解除綁定後,該bound servive即結束(destroyed)。

WorkManager

WorkManager是API 28出現的library,它的功能是定義work、儲存work,並於適當時間執行work。官方頁面在此、官方的教學文在此,而我覺得寫得最好的是這篇官方的Medium

在介紹核心的class之前,先來看它運作的方式,下圖取自官方Medium的文章:

上圖簡單來說就是,先用WorkRequest定義work,接著將此work透過Internal TaskExecutor存進Database,接著當該work的執行條件被滿足的時候,Internal TaskExecutor會叫WorkerFactory去產生一個Worker,並由Executor去呼叫Worker的doWork()來執行這個work。
以下我只重點介紹其中幾個角色

  • WorkRequest 這個是work的具體規格,官方頁面在此。除了包含一個Worker(稍後會提到)之外,這裡也定義了work的類型(OneTimeWorkRequest或是PeriodicWorkRequest)以及條件(Constraints)。類型就是照字面上的意思,應該不太需要解釋,而限制的意思是在什麼情況下該work要被執行,只有在所有條件都吻合的時候該work才會被執行,而當有任何一個條件不吻合時就會停止這個work。

  • Worker 這是work的執行內容,官方頁面在此。它最重要的就是他的doWork()這個method,這個method有點像是Runable的run(),裡面放的就是這個work真正要被執行的程式碼。在doWork()裡面只能放同步的程式,若要異步的話要使用ListenableWorker。有兩點值得注意,一是doWork()最多只能跑十分鐘,超過的話該Worker就會接收到停止的訊號並停止;二是當doWork()內的工作做完後,該Worker就會被刪掉(destroyed)。

  • Executor 執行這個work的角色,他會去呼叫Worker的doWork()。預設的Executor會在main thread以外的thread,同步地執行這個doWork()。

thread

thread就沒有在Android官方建議中被提到了,我想可能是因為它有點太基礎,沒有跟lifecycle有相關綁定的功能,用起來其實蠻危險的,所以官方才沒有特別提到它。但不得不說,在Android裡面要做背景運算的話一定都還是會用到thread的,只是被其他東西包了起來或是搭配其他東西使用而已。關於thread我有寫另一篇文章,這邊我就不重複了。

心得

照官方的建議,把工作分成Immediate、Deferred和Exact是很清楚,但就實作面來說還不太夠。在實作上還有這工作是一次性的還是是有一系列相關的工作要常駐在背景,這會影響到程式的架構。以下是我自己的心得,會比較主觀,每個人想法不同或許會有不同的選擇,可以把下面心得當個參考就好。

  • 一次性且輕量的工作
    能用Kotlin coroutine的話就都用coroutine,把它跟Activity或Fragment或ViewModel的lifecycle綁在一起用。

  • 一系列相關的一次性輕量工作
    可以寫一個bound service裡面用thread或是寫一個helper class在裡面用thread。這兩者是有差的,但以現今主流的App架構來說,寫得好的話這兩者幾乎可以算是一樣的東西。
    先講結論:這類型的實作直接選擇bound service裡面放thread
    這兩者的差異主要有二:

    1. 橫跨不同Activity在背景運作 若App有多個Activity的話bound service可以在多個Activity的lifecycle中存活,用thread也行但在不同Activity間啟動與停止會提升維護的複雜度,容易產生問題。不過自從Android官方在Android Dev Summit '18中提倡single activity application之後,單一Activity的App變成主流,所以關於多個不同Activity之間的議題也就不存在了。
    2. 不同process間的IPC 若App有多個process在運行的話,那thread就沒辦法跨越process進行存取了,而寫這時候用thread要格外小心,很容易會出問題。而bound service本身的設計就有包含跨process時的處理,也有IPC的機制,所以如果要跨process的話最好是用bound service。但一般來說,一個App很少會用到不同的process,所以這個議題通常也不用考慮到。
  • 一次性且持續時間較長的工作
    先講結論:直接選擇WorkManager。當然也可以跟上面一樣用bound servie裡面放thread,或是用foreground service,但他們兩個都有些缺點。但前者若所有綁定的component都結束了它就會被強制結束;後者是一定要在notification bar佔一個位置,若是使用情境有需要的話當然很好,但若只是一般的運算也是沒必要放在那麼顯眼的notification bar上。

  • 重複性的工作
    毫無懸念,秒選WorkManager

  • 定時執行的工作
    毫無懸念,秒選AlarmManager

  • 需要長時間在背景待命的工作
    這個就只能用foreground service了,就像防毒軟體一樣會常駐在notification bar。