Android Jetpack – 使用 WorkManager 管理後臺任務

SouthernBox發表於2019-03-04

作為 Android Jetpack 中的新元件,WorkManager 負責用來管理後臺任務,它和一個非同步任務以及 Service 有什麼區別呢?看完你就知道了。

相關類

我們先來看看 WorkManager 相關的幾個類:

  • Worker
    任務的執行者,是一個抽象類,需要繼承它實現要執行的任務。

  • WorkRequest
    指定讓哪個 Woker 執行任務,指定執行的環境,執行的順序等。
    要使用它的子類 OneTimeWorkRequest 或 PeriodicWorkRequest。

  • WorkManager
    管理任務請求和任務佇列,發起的 WorkRequest 會進入它的任務佇列。

  • WorkStatus
    包含有任務的狀態和任務的資訊,以 LiveData 的形式提供給觀察者。

接下來是 WorkManager 的簡單使用。

使用

WorkManager 的實現包括以下幾個步驟。

依賴

在 build.gradle 新增如下依賴:

implementation "android.arch.work:work-runtime:$work_version"
implementation "android.arch.work:work-firebase:$work_version"
複製程式碼

定義 Worker

我們定義 MainWorker 繼承 Worker,發現需要重寫 doWork 方法,並且需要返回任務的狀態 WorkerResult:

class MainWorker : Worker() {
    override fun doWork(): WorkerResult {
        // 要執行的任務
        return WorkerResult.SUCCESS
    }
}
複製程式碼

我們暫時什麼都不做,直接返回任務執行完成 WorkerResult.SUCCESS。

定義 WorkRequest

在 MainActivity 中定義 WorkRequest:

val request = OneTimeWorkRequest.Builder(MainWorker::class.java).build()
複製程式碼

OneTimeWorkRequest 意味著這個任務只需執行一遍。

加入任務佇列

要讓任務執行,需要將 WorkRequest 加入任務佇列:

WorkManager.getInstance().enqueue(request)
複製程式碼

現在加入任務佇列後,任務會馬上得到執行。但需要注意的是,這句程式碼的作用是將任務加入任務佇列,而不是執行任務,至於區別後面會講到。

資料互動

後臺任務少不了資料的互動,我們看一下資料是如何傳入傳出的。

input

先是在 Activity 傳資料給 Worker ,我們傳一個格式化過的時間過去:

val dateFormat = SimpleDateFormat("hh:mm:ss", Locale.getDefault())

val data = Data.Builder()
        .putString("time", dateFormat.format(Date()))
        .build()

val request = OneTimeWorkRequest.Builder(DemoWorker::class.java)
        .setInputData(data)
        .build()
複製程式碼

使用 WorkRequest 的 setInputData 方法傳遞 Data,Data 的使用和 Bundle 差不多。

在 Worker 中,從 inputData 可以取到資料,這裡取到後簡單列印一下:

class MainWorker : Worker() {
    override fun doWork(): WorkerResult {
        Log.d("WorkManager", inputData.getString("time",""))
        return WorkerResult.SUCCESS
    }
}
複製程式碼

output

當任務處理完了,需要將處理結果返回。傳入的是 inputData,傳出就是 outputData:

class MainWorker : Worker() {
    override fun doWork(): WorkerResult {
        Log.d("MainWorker", inputData.getString("time",""))
        outputData = Data.Builder()
            .putString("name", "SouthernBox")
            .build()
        return WorkerResult.SUCCESS
    }
}
複製程式碼

每一個 WorkRequest 都會有一個 id,通過 id 可以獲取到對應任務的 WorkStatus,並且是以 LiveData 形式提供的:

WorkManager.getInstance()
        .getStatusById(request.id)
        .observe(this, Observer { workStatus ->
            if (workStatus != null && workStatus.state.isFinished) {
                Log.d("MainActivity", workStatus.outputData.getString("name", ""))
            }
        })
複製程式碼

如果需要取消一個在佇列中的任務,也是通過 id 實現的:

WorkManager.getInstance().cancelWorkById(request.id)
複製程式碼

這樣我們就完成了一個最簡單的 WorkManager,執行一下就可以看到列印的結果了。

特性

到目前為止都是基本操作,和一個普通的非同步任務沒有太大區別,接下來我們看看它不一樣的一些地方。

環境約束

WorkManager 允許我們指定任務執行的環境,比如網路已連線、電量充足時等,在滿足條件的情況下任務才會執行。

可指定的條件及設定方法如下:

val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)  // 網路狀態
        .setRequiresBatteryNotLow(true)                 // 不在電量不足時執行
        .setRequiresCharging(true)                      // 在充電時執行
        .setRequiresStorageNotLow(true)                 // 不在儲存容量不足時執行
        .setRequiresDeviceIdle(true)                    // 在待機狀態下執行,需要 API 23
        .build()

val request = OneTimeWorkRequest.Builder(MainWorker::class.java)
        .setConstraints(constraints)
        .build()
複製程式碼

這個很好理解,除了網路狀態,其他設定項都是傳入一個布林值,網路狀態可選值如下:

狀態 說明
NOT_REQUIRED 沒有要求
CONNECTED 網路連線
UNMETERED 連線無限流量的網路
METERED 連線按流量計費的網路
NOT_ROAMING 連線非漫遊網路

我們試一下效果,新增一個需要聯網的條件,Activity 程式碼如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()

        val dateFormat = SimpleDateFormat("hh:mm:ss", Locale.getDefault())
        val data = Data.Builder()
                .putString("date", dateFormat.format(Date()))
                .build()

        val request = OneTimeWorkRequest
                .Builder(MainWorker::class.java)
                .setConstraints(constraints)
                .setInputData(data)
                .build()

        WorkManager.getInstance().enqueue(request)

        WorkManager.getInstance()
                .getStatusById(request.id)
                .observe(this, Observer<WorkStatus> { workStatus ->
                    if (workStatus != null && workStatus.state.isFinished) {
                        Log.d("MainActivity",
                                workStatus.outputData.getString("name", ""))
                    }
                })

    }
}
複製程式碼

開啟應用之前,先把網路關閉,開啟後發現 Worker 並沒有列印時間,這時候再把網連上,就會看到列印出時間了。

這也是為什麼前面說 WorkManager.getInstance().enqueue(request) 是將任務加入任務佇列,並不代表馬上執行任務,因為任務可能需要等到滿足環境條件的情況才會執行。

強大的生命力

還是一樣的程式碼,我們來做點不一樣的操作:

  1. 斷網後執行
  2. 將程式殺掉
  3. 聯網
  4. 再次執行

不出意外的話,這時候你會看到有兩個時間的列印,而且兩個時間還不一樣,這是為什麼呢?

第一個時間是第一次執行後,加入了任務佇列,但還沒有執行的任務。第二個則是本次執行的任務列印的。這說明了,就算程式被殺掉,任務還是存在,甚至如果重啟手機,任務依然會在滿足條件的情況下得到執行。

這是 WorkManager 的另一個特點,一旦發起一個任務,任務是可以保證一定會被執行的,就算退出應用,甚至重啟手機都阻止不了他。但可能由於新增了環境約束等原因,它執行的時間是不確定的。

當應用正在執行時,它會在當前的程式中啟用一個子執行緒執行。應用沒有執行的情況下啟用,它則會自己選擇一種合適的方式在後臺執行。具體是什麼方式和 Android 的版本和依賴環境有關:

Android Jetpack – 使用 WorkManager 管理後臺任務

定時任務

前面說了 OneTimeWorkRequest 是指任務只需要執行一遍,而 PeriodicWorkRequest 則可以發起一個多次執行的定時任務:

val request = PeriodicWorkRequest
        .Builder(MainWorker::class.java, 15, TimeUnit.MINUTES)
        .setConstraints(constraints)
        .setInputData(data)
        .build()
複製程式碼

這樣,發起的任務就會每隔 15 分鐘執行一次。除了需要傳入間隔時間,使用起來跟 OneTimeWorkRequest 是沒有區別的。

你可能會想更頻繁的去執行一個任務,比如幾秒鐘執行一遍,但很遺憾,最小時間間隔就是 15 分鐘,看一下原始碼就知道了。

還有需要注意的是,定時任務並不是說經過指定時間後它就馬上執行,而是經過這一段時間後,等到滿足約束條件等情況時,它才執行。

任務鏈

WorkManager 允許我們按照一定的順序執行任務,比如我想 A、B、C 三個任務按先後順序執行:

Android Jetpack – 使用 WorkManager 管理後臺任務

可以這樣寫,把它們組成一條任務鏈:

WorkManager.getInstance()
        .beginWith(workA)
        .then(workB)
        .then(workC)
        .enqueue()
複製程式碼

這樣的話,上一個任務的 outputData 會成為下一個任務的 inputData。

再更復雜一點,我想 A 和 B 同時執行,它們都執行完之後,再執行 C:

Android Jetpack – 使用 WorkManager 管理後臺任務

也是可以實現的:

WorkManager.getInstance()
        .beginWith(workA,workB)
        .then(workC)
        .enqueue()
複製程式碼

再更更復雜一點,如果我想這樣:

Android Jetpack – 使用 WorkManager 管理後臺任務

這樣就需要先把 A、B 和 C、D 分別組成一條任務鏈,再進行聯結:

val chain1 = WorkManager.getInstance()
        .beginWith(workA)
        .then(workB)
val chain2 = WorkManager.getInstance()
        .beginWith(workC)
        .then(workD)
val chain3 = WorkContinuation
        .combine(chain1, chain2)
        .then(workE)
chain3.enqueue()
複製程式碼

再更更更復雜一點,如果我把定時任務放進去會怎樣?不好意思,鏈式任務只支援 OneTimeWorkRequest。

使用任務鏈,我們可以將各種任務進行模組化。同樣的,任務鏈不保證每個任務執行的時間,但是保證它們執行的先後順序。

任務唯一性

很多情況下,我們希望在任務佇列裡,同一個任務只存在一個,避免任務的重複執行,這時候可以用到 beginUniqueWork 這個方法:

WorkManager.getInstance()
        .beginUniqueWork("unique", ExistingWorkPolicy.REPLACE, request)
        .enqueue()
複製程式碼

需要傳入一個任務的標籤,和重複任務的執行方式,可取值如下:

狀態 說明
REPLACE 刪除已有的任務,新增現有的任務
KEEP 什麼都不做,不新增新任務,讓已有的繼續執行
APPEND 加入已有任務的任務鏈最末端

但這種方式也是隻支援 OneTimeWorkRequest。如果是 PeriodicWorkRequest,我想到的辦法是每次執行之前,根據標籤去取消已有的任務。

以上,就是本文對 WorkManager 的簡單介紹和用法講解。

保活?

這裡引入一個思考,既然 WorkManager 的生命力這麼強,還可以實現定時任務,那能不能讓我們的應用生命力也這麼強?換句話說,能不能用它來保活?

要是上面有細看的話,你應該已經發現這幾點了:

  • 定時任務有最小間隔時間的限制,是 15 分鐘
  • 只有程式執行時,任務才會得到執行
  • 無法拉起 Activity

總之,用 WorkManager 保活是不可能了,這輩子都不可能保活了。

使用場景?

很明顯,WorkManager 區別於非同步任務,它更像是一個 Service。基本上,WorkManager 能做的,Service 也能做,我並沒有想到有什麼情況是非用 WorkManger 不可的。

但反觀 Service,氾濫的 Service 後臺任務可能是引起 Android 系統卡頓的主要原因,這幾年 Google 也對 Service 也做了一些限制。

對 Service 的限制

Android 6.0 (API 23)

休眠模式:在關閉手機螢幕後,系統會禁止應用的網路請求等功能。

Android 8.0(API 26)

在某些不被允許的情況下,呼叫 startService 會拋異常。

但目前很多 APP 的 target API 還在 23 以下,因為不想處理執行時許可權,更別說 API 26 了。基於此,2017 年年底,谷歌採取了少有的強硬措施。

對 Target API 的要求

2018 年 8 月起

所有新開發的應用,Target API 必須是 26 或以上。

2018 年 11 月起

所有已釋出的應用,Target API 必須更新到 26 或以上。

2019 年起

每次釋出新版本後,所有應用都必須在一年內將 Target API 更新到最新版本

雖然這些措施對國內的環境沒有辦法造成直接影響,但這也會成為一種發展指導方向。

更合理的後臺任務管理

說了這麼多,我想表達的是,在不久的將來,在某些情況下,Service 已經沒鳥用了!

而 WorkManager 作為一個更合理的後臺任務管理庫,在這種情況下就是一個更好的選擇了。

相關文章