在本系列的前四篇,我們從WorkManager初始化流程開始,一路提到基本流程、串接Worker的流程,到Constraints的運作流程,將主要的功能看過了一回。
本篇將是這系列最後一篇,於是來談一個Google開發人員在IO上只用一句話帶過的一件事:
Cancal is a best-effort operation  
Best-effort,簡而言之就是盡可能完成,但不保證能達到其原本的用途。代表WorkManage的所有cancel相關的實作,都不能保證一定能正確取消任務。
Why a Worker can’t be canceled immediately? 於是接下來將從簡單的範例出發:
WorkManager.getInstance().cancelUniqueWork(Worker.name); 
看到本篇,應該熟知要再看到WorManagerImpl:
@Override public  void  cancelUniqueWork (@NonNull  String uniqueWorkName)      mTaskExecutor.executeOnBackgroundThread(CancelWorkRunnable.forName(uniqueWorkName, this )); } 
直接透過Executor來執行CancelWorkRunnable。其實看到這邊就知道為什麼事best-effort了,原因是回到WorkManagerImpl的constructor:
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public  WorkManagerImpl (@NonNull  Context context, @NonNull  Configuration configuration,  boolean  useTestDatabase)    ...     mTaskExecutor = WorkManagerTaskExecutor.getInstance();     ... } 
直接看到WorkManagerTaskExecutor:
public  class  WorkManagerTaskExecutor  implements  TaskExecutor      ...     private  final  TaskExecutor mDefaultTaskExecutor = new  DefaultTaskExecutor();     ... } 
再看到DefaultTaskExecutor:
public  class  DefaultTaskExecutor  implements  TaskExecutor      private  final  ExecutorService mBackgroundExecutor = Executors.newSingleThreadExecutor();     ...     @Override      public  void  executeOnBackgroundThread (Runnable r)           mBackgroundExecutor.execute(r);     } } 
看到熟悉的函式executeOnBackgroundThread,和Executors.newSingleThreadExecutor(),可知WorkManagerImpl的Executor如果沒有透過外部指定,預設就是異步執行Runnable的Executor一個Worker要被取消,一定要先透過WorkManagerImpl執行,所以在單一Thread的Executor排程下,CancelWorkRunnable勢必會在佇列中等待,而無法立即被觸發來取消對應的Worker。
How does the Cancel operation works? 接著來看看cancel怎麼運作的,接續上面的內容,看到CancelWorkRunnable.forName():
public  static  Runnable forName (@NonNull  final  String name, @NonNull  final  WorkManagerImpl workManagerImpl)      return  new  CancelWorkRunnable() {         @WorkerThread          @Override          public  void  run ()               ...             List<String> workSpecIds = workSpecDao.getUnfinishedWorkWithName(name);             for  (String workSpecId : workSpecIds) {                 cancel(workManagerImpl, workSpecId);             }             ...             reschedulePendingWorkers(workManagerImpl);         }     }; } 
從資料庫中取出對應的WorkSpec,然後呼叫cancel():
void  cancel (WorkManagerImpl workManagerImpl, String workSpecId)      recursivelyCancelWorkAndDependents(workManagerImpl.getWorkDatabase(), workSpecId);     Processor processor = workManagerImpl.getProcessor();     processor.stopAndCancelWork(workSpecId);     for  (Scheduler scheduler : workManagerImpl.getSchedulers()) {         scheduler.cancel(workSpecId);     } } 
recursivelyCancelWorkAndDependents()不再贅述,簡言之就是將所有和WorkSpec直接相關或是間接相關的WorkSpec全都設定是CANCEL。也就是今天如有一個Worker鏈,取消其中一個,會全部一起被取消。
接著呼叫Processor的stopAndCancelWork():
public  synchronized  boolean  stopAndCancelWork (String id)      ...     mCancelledIds.add(id);     WorkerWrapper wrapper = mEnqueuedWorkMap.remove(id);     if  (wrapper != null ) {         wrapper.interrupt(true );         ...     }     ... } 
這邊將Processor內正在排隊中的Worker刪除,並放入代表已經被取消的列表中。然後呼叫interrupt()嘗試停止WorkerWrapper:
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public  void  interrupt (boolean  cancelled)      mInterrupted = true ;          if  (mWorker != null ) {         mWorker.stop(cancelled);     } } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public  final  void  stop (boolean  cancelled)      mStopped = true ;     mCancelled = cancelled;     onStopped(cancelled); } 
可看得出這些停止的動作只是設定了一些參數,並沒有主動停止Worker的執行。所以在自訂Worker時,則需要自行判斷是否中斷當前作業。這點其實跟透過自訂的Thread和Runnable,來實作多執行緒一樣,中斷的時機有一部分是自己掌控。
回到前面,最後再看到scheduler.cancel(),這裡一樣用GreedyScheduler當例子:
@Override public  synchronized  void  cancel (@NonNull  String workSpecId)      ...     mWorkManagerImpl.stopWork(workSpecId);     removeConstraintTrackingFor(workSpecId); } 
在看到stopWork():
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public  void  stopWork (String workSpecId)      mTaskExecutor.executeOnBackgroundThread(new  StopWorkRunnable(this , workSpecId)); } 
到這CancelWorkRunnable的任務基本上就結束了,最後會透過reschedulePendingWorkers再次透過Schedulers啟動資料庫中依然是ENQUEUED的WorkSpec:
void  reschedulePendingWorkers (WorkManagerImpl workManagerImpl)      Schedulers.schedule(                 workManagerImpl.getConfiguration(),                 workManagerImpl.getWorkDatabase(),                 workManagerImpl.getSchedulers()); } 
What’s more 最後,我們再來看看最後被啟動的StopWorkRunnable:
@Override public  void  run ()      ...     try  {         if  (workSpecDao.getState(mWorkSpecId) == State.RUNNING) {             workSpecDao.setState(State.ENQUEUED, mWorkSpecId);         }         boolean  isStopped = mWorkManagerImpl.getProcessor().stopWork(mWorkSpecId);         ...     } } 
實際上,我還無法理解為何要再執行這個Runnable。任何中斷的設定:WorkSpec資料庫和停止WorkWrapper,都已經在cancel()執行完畢。初步猜測應是有一些時間差的問題,導致需要再透過Scheduler來確保所有任務有被盡可能的即時停止。
Summary 到此我們完成初步的分析,但由於查看的版本還只是alpha版,所以這邊只是給出一個初步的概念。離正式版號還有一段距離,可以預期到時還要重新研究,並修改前面的程式片段和分析內容。