Android WorkManager

本文主要內容

WorkManager使用詳細介紹
自定義Worker的幾種實現
使用注意事項

特性

  • 兼容Android API 14+
  • API 23+使用JobScheduler
  • API 14-22 使用BroadcastReceiver + AlarmManager
  • 可配置任務執行約束條件(比如在有網絡時,充電時,電量充足時等)
  • 執行一次性任務和周期性任務
  • 跟蹤管理任務:取消任務,獲取任務結果
  • 支持串聯執行任務,可設置任務執行順序
  • 保證任務執行,即使是app或者設備重啟了

關鍵類

  • WorkManager
    采用單例模式(WorkManager.getInstance()),用于任務管理(添加,取消,獲取任狀態果,結果等)
  • Worker
    抽象任務類,任務抽象方法為:doWork()
  • WorkRequest :
    任務請求抽象類,WorkManager通過WorkRequest添加任務,其實現類有:OneTimeWorkRequest和PeriodicWorkRequest
  • OneTimeWorkRequest
    一次性任務請求
  • PeriodicWorkRequest
    周期性任務請求(注意:周期性任務最小間隔時間為15分鐘)
  • Constraints
    執行任務的約束條件,比如需要充電狀態才執行(mRequiresCharging),電量充足時才執行(mRequiresBatteryNotLow)等等
  • WorkInfo
    任務相關信息,包括WorkRequest產生的ID(UUID),當前任務狀態,結果數據,標識Tag,通過:WorkManager.getWorkInfoById(UUID) or WorkManager.getWorkInfoByIdLiveData(UUID)獲得

使用方法

添加WorkManager依賴

dependencies {
    def work_version = 2.0.1
    // (Java only)
    implementation "androidx.work:work-runtime:$work_version"
    
    // Kotlin + coroutines
    implementation "androidx.work:work-runtime-ktx:$work_version"
    
    // optional - RxJava2 support
    implementation "androidx.work:work-rxjava2:$work_version"
    
    // optional - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"
 }

WorkManager 初始化

默認系統已經給我們初始化了WorkManager,無需自己初始化。但如果要自己初始化,需要在AndroidManifest.xml文件里面添加以下標簽刪除默認初始化:

<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    tools:node="remove" />

然后在Application.onCreate()中初始化WorkManager,通過WorkManager.getInstance()獲取實例,初始化如下:

// provide custom configuration
Configuration myConfig = new Configuration.Builder()
    // 設置日志級別
    .setMinimumLoggingLevel(android.util.Log.INFO)
    // 設置后臺任務線程池
    .setExecutor(Executors.newFixedThreadPool(8))
    .build();

// 初始化 WorkManager
WorkManager.initialize(this, myConfig);
// 獲取實例
WorkManager workManager = WorkManager.getInstance()

執行一次性任務: OneTimeWorkRequest

  • 執行任務前,首先需自定任務,繼承Worker抽象類,實現抽象方法doWork()即可,doWork()就是任務方法(有點類似Runnable接口的run()方法):
public class MyWorker extends Worker {

    public MyWork(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @Override
    public Result doWork() {
        Log.d(TAG, "I am working");
        // 任務結果
        Data data = new Data.Builder().putString("result", "I am result from MyWork").build();
        // 成功返回任務結果,實際結果包含在data對象里面,如果無需返回結果返回:Result.success()
        // 失敗返回:Result.failure()
        // 需要重新執行返回:Result.retry()
        return Result.success(data);
    }
}
  • 第二步創建WorkRequest,執行一次性任務使用:OneTimeWorkRequest
// 每個WorkRequest會初始化一個UUID作為唯一任務ID,查詢任務狀態或者取消任務都需要通過這個ID來實現
OneTimeWorkRequest request = new OneTimeWorkRequest
    // 任務為MyWork
    .Builder(MyWork.class)
    // 設置執行條件為充電時才執行
    .setConstraints(new Constraints.Builder().setRequiresCharging(true).build())
    // 設置延遲5分鐘執行
    .setInitialDelay(5, TimeUnit.MINUTES)
    // 添加tag,用于標識任務,可以通過tag查詢任務狀態,取消任務等操作
    .addTag("one_time_request_tag")
    // 設置輸入data,有點像輸入參數,可以在Worker中通過getInputData()方法獲取
    .setInputData(new Data.Builder().putString("input_data", "I am input data").build())
    .build();
  • 第三步將OneTimeWorkRequest添加到任務隊列
// 添加任務到隊列,任務會在條件滿足的情況下才會執行 
WorkManager.getInstance().enqueue(request);

添加周期性任務:PeriodicWorkRequest

步驟跟添加一次性任務一樣,只是WorkRequest從OneTimeRequest改為PeriodicWorkRequest,示例如下:

// 創建每隔30分鐘執行一次的周期性任務請求
PeriodicWorkRequest request = new PeriodicWorkRequest
    .Builder(MyWork.class, 30, TimeUnit.MINUTES)
    .build();
// 添加任務
WorkManager.getInstance().enqueue(request);

\color{#FF0000}{注意:PeriodicWorkRequest周期性任務最小間隔時間為15分鐘}

任務鏈(組合任務,關聯任務)

  • 可以將多個任務組合起來,可設置執行的先后順序
  • 任務之間的結果又輸入輸出關系,上一個任務的輸出數據會是下一個任務輸入數據,可通過Worker.getInputData()獲取輸入數據,doWork()方法的返回值為輸出數據:Result.success(data)
  • 任務鏈只能用于OneTimeWorkRequest

比如有四個任務A,B,C,D,我們需要先執行完A和B兩個任務,在執行C任務,最后在執行D任務,代碼如下:

WorkManager.getInstance()
    // 先執行 A,B,并行執行
    .beginWith(Arrays.asList(A, B))
    // 等 A 和 B 都執行完成后,再執行 C
    .then(C)
    // 等C執行完,再執行 D
    .then(D)
    // Don't forget to enqueue()
    .enqueue();

上面提到任務之間數據的輸入輸出關系,如果某個任務的前置任務是由多人任務組成,比如上面的例子:任務A和B的結果都會傳遞到任務C中,但任務結果Data對象是通過key-value的方式存儲數據,如果存在相同的key結果如何組合傳遞給C,所以這里需要一個組合策略InputMerger,在這里WorkManager提供了兩種實現:

  • OverwritingInputMerger 如果存在相同的key,就會覆蓋
  • ArrayCreatingInputMerger 合并結果,組合成一個列表

我們在創建任務C的WorkRequest時可通過setInputMerger()方法設置輸入數據組合規則

unique 任務(唯一的任務)

unique任務在某種場景下會很實用,有點類似單例,指定名稱的任務/任務鏈只有一個;再次添加相同名稱的任務時,可以有幾種處理方式:

  • KEEP :保留原來的任務,新添加的任務會被忽略
  • REPLACE :替換原來的任務,原來的任務會被取消掉
  • APPEND :等原來的任務執行完后,再執行新添加的任務(這里貌似對唯一性的理解有些牽強,可以理解成正執行任務唯一性)

使用方法如下,主要調用:enqueueUniqueWork方法

// 唯一任務
WorkManager.getInstance()
    // my_work 唯一任務標識,ExistingWorkPolicy(KEEP,REPLACE,APPEND)
    .enqueueUniqueWork("unique_work_name", ExistingWorkPolicy.REPLACE, request);

// 唯一任務鏈
WorkManager.getInstance()
    .beginUniqueWork("unique_work_chain_name", ExistingWorkPolicy.REPLACE, Arrays.asList(requestA, requestB))
    .then(requestC)
    .then(requestD)
    .enqueue();

獲取任務狀態,結果,取消任務

在任務整個生命周期中,有如下一些狀態:

  • ENQUEUED :進入隊列狀態,在條件滿足的時候就會執行
  • RUNNING :正在執行
  • SUCCEEDED :doWork()返回Result.success() 后的狀態,表明任務執行成功,這種狀態只會在OneTimeWorkRequest中出現
  • FAILED :doWork()返回Result.failure() 后的狀態,表示任務執行失敗,也只會在OneTimeWorkRequest中出現
  • BLOCKED : 阻塞狀態,一般出現在前一個任務還沒有執行完(在unique任務的APPEND操作中)
  • CANCELLED :當通過WorkManager取消一個沒有執行完成的任務后的狀態
獲取任務信息:WorkInfo
  • WorkInfo包含了: 任務id(UUID), 任務狀態(State),任務結果(Data),任務tag(List<String>)
public WorkInfo(UUID id, State state, Data outputData, List<String> tags) {
    mId = id; // WorkRequest 的ID
    mState = state; // 任務當前狀態
    mOutputData = outputData; // 任務執行結果
    mTags = new HashSet<>(tags); // 任務tag,在創建WorkRequest設置的
}

WorkManager提供了以下的方法查詢WorkInfo信息,并提供了ListenableFuture和LiveData兩種結果返回方式:

ListenableFuture<WorkInfo> getWorkInfoById(UUID id)
LiveData<WorkInfo> getWorkInfoByIdLiveData(UUID id)

ListenableFuture<List<WorkInfo>> getWorkInfosByTag(String tag)
LiveData<List<WorkInfo>> getWorkInfosByTagLiveData(String tag)

ListenableFuture<List<WorkInfo>> getWorkInfosForUniqueWork(String uniqueWorkName);
LiveData<List<WorkInfo>> getWorkInfosForUniqueWorkLiveData(String uniqueWorkName)

獲取任務信息結合LiveData:

WorkManager.getInstance().getWorkInfoByIdLiveData(myWorkRequest.getId())
    .observe(lifecycleOwner, new Observer<WorkInfo>() {
        @Override
        public void onChanged(@Nullable WorkInfo workInfo) {
          if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
              displayMessage("You did good job!")
          }
        }
    });
取消任務
  • 取消任務框架并不能保證任務不被執行(比如有些任務已經開始執行了或者已經執行完成了)
  • 如果是耗時任務,在我們實現Worker的doWork()方法時可通過isStopped()方法檢測任務是否已被停止,如果被停止了則結束任務執行,并回收資源

取消任務方法如下:

WorkManager.cancelAllWork()  取消所有任務
WorkManager.cancelWorkById(UUID id)  通過ID取消任務
WorkManager.cancelAllWorkByTag(String tag) 通過tag取消相關任務
WorkManager.cancelUniqueWork(String uniqueWorkName) 通過標識取消唯一任務

WorkManager的Work實現

WorkManager提供了4種Work:Worker, CoroutineWorker, RxWorker, ListenableWorker

  • Worker

WorkManager會在后臺線程執行我們的任務,后臺線程來自默認初始化時Configuration創建的Executor,當然我們也可以自己初始化WorkManager,在上面初始化WorkManager有講到:

WorkManager.initialize(context,
    new Configuration.Builder()
        .setExecutor(Executors.newFixedThreadPool(8))
            .build());
  • CoroutineWorker

CoroutineWorker是Kotlin提供優秀的協程實現,示例如下:

class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result = coroutineScope {
        val jobs = (0 until 100).map {
            async {
                downloadSynchronously("https://www.google.com")
            }
        }

        // awaitAll will throw an exception if a download fails, which CoroutineWorker will treat as a failure
        jobs.awaitAll()
        Result.success()
    }
}

在這里doWork()是阻塞方法,CoroutineWorker跟Worker不一樣,它不在Executor中執行,而是通過CoroutineDispatcher執行,默認提供了Dispatchers.IO來執行后臺任務,當然你也可以自定義CoroutineDispatcher(通過重載CoroutineWorker變量:open val coroutineContext 實現)

  • RxWorker

顧名思義是為RxJava提供的一種實現,通過繼承RxWorker實現createWork()方法,返回的是RxJava中可訂閱對象:Single<Result>,跟Observable差不多

public class RxDownloadWorker extends RxWorker {

    public RxDownloadWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @Override
    public Single<Result> createWork() {
        return Observable.range(0, 100)
            .flatMap { download("https://www.google.com") }
            .toList()
            .map { return Result.success() };
    }
}

注意createWork()方法是在主線程調用,返回結果在后臺線程,通過重載 RxWorker.getBackgroundScheduler()自定義后臺線程

  • ListenableWorker

ListenableWorker提供更底層的任務執行細節,其任務執行的入口方法是startWork()

public abstract @NonNull ListenableFuture<Result> startWork();

上面三種Worker都繼承自它,通過實現startWork方法,執行任務不同調度方式,并抽象了具體任務doWork()方法,比如Worker的實現是:

public abstract @NonNull Result doWork();

@Override
public final @NonNull ListenableFuture<Result> startWork() {
    mFuture = SettableFuture.create();
    getBackgroundExecutor().execute(new Runnable() {
        @Override
        public void run() {
            try {
                Result result = doWork();
                mFuture.set(result);
            } catch (Throwable throwable) {
                mFuture.setException(throwable);
            }
        }
    });
    return mFuture;
}

如果你需要自己控制任務的執行(執行任務的線程),可以直接繼承ListenableWorker,并是現實startWork()方法,示例:

public class MyThreadWorker extends ListenableWorker {

    public MyThreadWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        @SuppressLint("RestrictedApi")
        final SettableFuture<Result> result = SettableFuture.create();
        new Thread(new Runnable() {
            @SuppressLint("RestrictedApi")
            @Override
            public void run() {
                Log.d(TAG, "i am doing work");
                result.set(Result.success());
            }
        }).run();
        return result;
    }
}

注意事項

  • Worker 都是通過反射實例化的,所以要注意自定義Worker的訪問權限,構造函數為public,內部類記得加上 public static 修飾符
  • WorkManager 適用于延期執行任務,或者是需要在app退出或者設備重啟后也能確保任務執行
  • WorkManager不適用場景:正在運行的進程提供后臺任務,需要立即執行的后臺任務
  • PeriodicWorkRequest最小間隔時間為15分鐘

更多內容見 官方文檔官方Demo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370