2023年5月15日月曜日

[android]app背景處理 with WorkManager

最近開始接觸android 13版之後,發現背景服務已經不能長存了。

總是app退到背景兩分鐘之後,service就會被系統砍掉。不管是不是用start_sticky標記。

android 8~11,使用setforeground的方式處理,已經讓開發者們怨聲載道了。(service啟動之後要在10秒內進行系統提醒channel的建立跟提醒的發送。) 

然後2021年出現了官方的解決方案:WorkManager。當時覺得還能動,剛出現的模組也不一定穩定,就沒留意。

但是現在不同了,service在app退到背景2分鐘之後就被砍了... 只能面對。所以又花了時間處理這部分。實際上沒那麼好解決,幾個要素交互影響不說,也需要測試裝置深眠狀態的反應。
寫下實作的方式,跟處理問題的時候遇到的狀況。有興趣可以參考。

能夠讓app盡可能啟動背景運作的方式

  • Google Cloud Messaging(以下簡稱「GCM」):假如有架伺服器的話,建議實作。這是權限相當高的動作,有能力啟動app的前景動作。
    要注意的是,GCM在android 12以後(不是很確定版本)不能使app在背景狀態之下啟動前景服務。就算是採用送Broadcast, 經由app在Manifest宣告的BroadcastReceiver觸發的方式,都會送你不能在背景狀態啟動前景服務的例外。
    那這樣不就廢了!?Orz
    目前的解決方案是導入「WorkManager」。
    上面有寫到,GCM有能力啟動app的前景服務。在實驗時發現app在背景狀態,甚至是裝置休眠狀態,裝置收到GCM訊息的當下,Worker會被喚起執行。這樣就有機會實作不容易被中斷的處理了。
  • WorkManager:本文的內容。在Service接近殘廢之後,目前的背景執行解決方案。
    可惜WorkManager也有缺點。因為是背景執行緒,在doWork()裡面不能執行必須在主執行序運作的動作。像是Lifecycle.addObserver,Livedata.addObserver等等...


決定要怎麼使用

基本的使用流程:

  1. 導入模組
  2. 撰寫Worker,定義執行內容
  3. 以Worker建立WorkRequest
  4. 把WorkRequest放進WorkManager  

詳細內容請參考官方文件:

https://developer.android.com/guide/background/persistent/getting-started

 

撰寫Worker

Worker分成兩種:

  • ListenableWorker:可以定義ListenableFuture,取得Worker的執行狀況。預設是在主執行緒執行。要在背景執行緒執行,需要做一些深入調整(主要是在AndroidManifest移除ListenableWorker預設的初始化工具,自行定義Configuration,指定executer為背景執行緒)。本文不研究這部分。
  • Worker:ListenableWorker的子class。預設在背景執行緒執行。缺點是除了模組提供的WorkInfo之外,沒有獲取執行狀況的方法。當然是可以用static singletone的方式暴力處理...

 

建立WorkRequest

WorkRequest有兩種:

  • OneTimeWorkRequest:只執行一次。若是在worker執行完畢回傳Result.Retry();,就會重試。重試的動作間隔是backoff的設計,可以定義linear(固定重試間隔) / exponential(指數增加間隔)
  • PeriodicWorkRequest:可以定義重複的間隔時間(固定間隔,跟每次執行可以跑多久)。最低的重複間隔是15分鐘... 相信有人會覺得不太合用...

WorkRequest可定義setExpedited(...),代表很重要,系統會儘快執行。會多快並不確定,筆者測試是毫秒等級,夠用了。
可使用constraint限制運作條件(例如是不是連網路,是不是在充電等等)。同時限制多個運作條件,就只有在條件同時滿足的時候才會執行。

 

依worker的管理方式區分

  • UniqueWork:指定一個字串id方便管理。要取代或是維持WorkRequest都使用這個字串做處理。
  • 非UniqueWork:還是可以使用取得WorkInfo的時候,回傳的uuid做處理。只是管理上相對麻煩。如何取得WorkInfo後面會介紹。
  • tag: WorkRequest可以定義tag,這樣在使用workManager.getInfosByTag(...)取得Workinfo的時候,可以用tag來取得同一個tag的所有workRequest的執行狀況。


依照需執行的動作的性質決定。本文只討論UniqueWork,大部分的場景已夠用。

建立Worker的例子:

public class MyWorker extends Worker
{
Context context;
// Worker的constructor會提供context。
public MyWorker(
Context context,
WorkerParameters params)
{
super(context, params);
this.context = context;
}


@Override
@NonNull
public Result doWork()
{
try
{
setForegroundAsync(getForegroundInfo());
} catch (Exception e)
{
e.printStackTrace();
}
...
}
@Override
@NonNull
public ForegroundInfo getForegroundInfo()
{

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
createChannel();
}

Log.d(TAG, "getForegroundInfo");


String NOTIFICATION_CHANNEL_ID = context.getPackageName();
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
Notification notification = notificationBuilder.setOngoing(true)
.setSmallIcon(R.drawable.app_logo)
.setContentTitle("背景執行中")
.setPriority(NotificationManager.IMPORTANCE_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
.build();

return new ForegroundInfo(0, notification);
}

@RequiresApi(Build.VERSION_CODES.O)
private void createChannel()
{
// Create a Notification channel
String NOTIFICATION_CHANNEL_ID = context.getPackageName();
String channelName = context.getResources().getString("背景執行中");
NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE);
chan.setLightColor(Color.BLUE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
assert notificationManager != null;
notificationManager.createNotificationChannel(chan);
}
}
 

建立OneTimeWorkRequest的例子:

OneTimeWorkRequest myWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).build();

把WorkRequest加入WorkManager的例子:

WorkManager workManager = WorkManager.getInstance(context); 
WorkContinuation wc = workManager.beginUniqueWork("myService", ExistingWorkPolicy.REPLACE, myWorkRequest);
wc.enqueue();

取得workManager必須有context。

 

注意點:

  • 每次Worker執行完畢,不管結果是成功,取消,或是重試,下一次的Worker執行都會重新建立一個新的Worker。也就是worker的constructor會被執行。
  • 針對android 11以前的版本的相容性,也就是那個惡名昭彰的「啟動前景服務,10秒內沒有發送系統通知的話,就會直接送你RuntimeException」的規格,
    Worker必須定義「getForegroundInfo(ForegroundInfo)」。
    實作範例請參考 「Support for long-running workers」裡面的實作方式。上面的程式碼也有概略的寫法。
  • 另外,若是要讓Worker長時間執行,建議下指令「setForegroundAsync(getForegroundInfo(progress));」
  • 針對android 12以後的狀況,「setForegroundAsync(getForegroundInfo(progress));」建議使用try / catch包覆,因為有可能會失敗。
    雖然官方Worker模組的2.7版(目前最新版是2.8.1)文件載明「setForegroundAsync()已經棄用」,還是建議寫一下,增加相容性。
  • android官方文件的「Support for long-running workers」裡面的範例程式碼,定義了「private createForegroundInfo()」。雖然這樣不算有問題,不過跟必須定義「getForegroundInfo()」的限制有落差。
    直接定義 「getForegroundInfo()」,然後範例內的
    「setForegroundAsync(createForegroundInfo(progress));」
    改為
    「setForegroundAsync(getForegroundInfo(progress));」
    就ok。

 

 如何取得WorkRequest的Workinfo (uuid,運作狀態等等)

  • 有定義UniqueWork id,可以用專有函式:ListenableFuture< List<WorkInfo> > lf = workManager.getWorkInfosForUniqueWork("myService");
  • 執行workManager.beginUniqueWork之後,以workContinuation.getWorkInfos()取得。以下為範例:
    WorkContinuation workContinuation = workManager.beginUniqueWork("myService", ExistingWorkPolicy.REPLACE, myWorkRequest);
    ListenableFuture< List<WorkInfo> > lf = workContinuation.getWorkInfos();
     

應該有發現取得workinfo的動作,回傳大都是List。這是因為使用WorkManager加入單一或是定時重複work之後,回傳的是「WorkContinuation」結構。前面的範例是直接workContinuation.enqueue(),因為只有一個work要做。

WorkRequest是允許連續動作的。可以設計其他的OneTimeWorkRequest,然後使用workContinuation.then(oneTimeWorkRequest)加入。等到全部都設定完之後,使用wc.getWorkInfos(),回傳的就是整串的workinfo。

這時進行workContinuation.enqueue()加入WorkManager,就可以循序執行一連串的work。
WorkRequest的chaining就不討論了。