玩轉全新的 Android 8.0 Oreo 後臺策略

at_1發表於2021-09-09

我們永遠都需要流暢的使用者體驗,但很遺憾我們手上的硬體資源卻總是和這個需求唱反調。這也是 Android 平臺不斷努力的切入點——從 API 26開始,Android 對後臺服務引入了嚴格的限制。基本上,除非您的應用在前臺執行,否則系統將在幾分鐘內停止應用的所有後臺服務


由於對後臺服務的這些限制,JobScheduler 已經成為執行後臺任務的實際解決方案。對於熟悉服務的開發者來說,JobScheduler 使用起來通常很簡單,當然也存在少量例外。我們這次就來探討其中一個例外。


假如您正在搭建一個 Android TV 應用。頻道對電視應用非常重要,因此您的應用需要能夠執行至少五種與頻道有關的後臺操作:釋出頻道,向頻道新增節目,將有關頻道的日誌傳送到遠端伺服器,更新頻道的後設資料,以及刪除頻道。在 Android 8.0(Oreo)之前,這五個操作中的每一個都可以在後臺服務中實現。然而,從 API 26 開始,您必須明智地決定,哪些應該沿用原有的普通後臺 Service,哪些應該使用 JobService。


如果只考慮電視 App 的使用場景,上述五個操作裡,其實只有 “頻道釋出” 可以做成一個原有的普通後臺服務。在某些場合下,頻道釋出涉及三個步驟:首先使用者單擊按鈕開始該過程; 然後,應用啟動後臺操作來建立和提交出版物; 最後,使用者通過使用者介面以確認訂閱。至此您可以看到,釋出頻道需要使用者互動,因此需要可見的 Activity。所以,ChannelPublisherService 可以是一個 IntentService,負責處理後臺邏輯。您不應該在這裡使用 JobService,因為 JobService 會引入延遲,而使用者互動通常需要您的應用進行即時響應。


對於其他四個操作,您應該使用 JobService; 因為它們都可以在您的應用位於後臺時執行。所以您應該分別建立 ChannelProgramsJobService,ChannelLoggerJobService,ChannelMetadataJobService,和 ChannelDeletionJobService。


避免 JobId 衝突

玩轉全新的 Android 8.0 Oreo 後臺策略

由於以上所有的四個 JobService 都在處理 Channel 物件,您似乎可以方便地使用 channelId 作為 jobId。但是由於 JobService 在 Android Framework 中設計的方式,您不能這樣做。以下是 jobId 的官方描述:


應用為這個作業提供的 ID。 隨後呼叫取消,或建立相同 jobId 的作業,
將會更新已經存在的同一個 ID 的作業。該 ID 在同一個 uid 的所有客戶端(不只是同一個應用包)中必須是唯一的。
您需要確保該 ID 在應用更新時始終保持穩定,因此它可能不應該基於資源 ID。 複製程式碼


根據以上的描述,即使您使用 4 個不同的 Java 物件(即 -JobService),也仍然不能使用 channelId來作為它們的 jobId。類級別的名稱空間不能幫助到您。


這確實是個問題。您需要一個穩定、可擴充套件的方式來將 channelId 和它的 jobId 關聯起來。而最糟的結果莫過於,由於 jobId 衝突,導致不同的頻道互相覆蓋操作。如果jobId 是 String 型別,而不是 Integer 型別的話,解決起來就很容易:ChannelProgramsJobService 的 jobId = "ChannelPrograms" + channelId, ChannelLoggerJobService 的 jobId = "ChannelLogs" + channelId,等等。但因為 jobId屬於 Integer 型別,而不屬於 String 型別,所以您就要設計一個智慧的系統,用來為您的作業生成可重複使用 jobId。


重點來了 —— 現在我們來聊聊 JobIdManager,看怎樣用它來解決這個問題。


JobIdManager 是一個類別,您可以根據自己的應用需求進行調整。對於目前談到的這個電視應用,基本構想是:使用一個 channelId 處理與 Channel 相關的所有作業 。下面我們先來看看這個樣本 JobIdManager 類的程式碼 ,然後再詳細討論。


public class JobIdManager {

   public static final int JOB_TYPE_CHANNEL_PROGRAMS = 1;
   public static final int JOB_TYPE_CHANNEL_METADATA = 2;
   public static final int JOB_TYPE_CHANNEL_DELETION = 3;
   public static final int JOB_TYPE_CHANNEL_LOGGER = 4;

   public static final int JOB_TYPE_USER_PREFS = 11;
   public static final int JOB_TYPE_USER_BEHAVIOR = 21;

   @IntDef(value = {
           JOB_TYPE_CHANNEL_PROGRAMS,
           JOB_TYPE_CHANNEL_METADATA,
           JOB_TYPE_CHANNEL_DELETION,
           JOB_TYPE_CHANNEL_LOGGER,
           JOB_TYPE_USER_PREFS,
           JOB_TYPE_USER_BEHAVIOR   })
   @Retention(RetentionPolicy.SOURCE)
   public @interface JobType {
   }

   //16-1 for short. Adjust per your needs
   private static final int JOB_TYPE_SHIFTS = 15;

   public static int getJobId(@JobType int jobType, int objectId) {
       if ( 0 < objectId && objectId < (1<< JOB_TYPE_SHIFTS) ) {
           return (jobType << JOB_TYPE_SHIFTS) + objectId;
       } else {
           String err = String.format("objectId %s must be between %s and %s",
                   objectId,0,(1<<JOB_TYPE_SHIFTS));
           throw new IllegalArgumentException(err);
       }
   }

}複製程式碼


如您所見,JobIdManager 只需結合一個字首和 channelId 即可獲得 jobId。然而這種簡單優雅的解決方案只是冰山一角。我們來考慮一下假設條件和注意事項。


您必須能夠強制 channelId 成為一個 Short 型別,所以當您將 channelId 與一個字首結合後,您仍然會得到一個有效的 Java Integer。當然,嚴格來說,它不一定是 Short。只要您的字首和 channelId 組合成一個不溢位的 Integer,它就能有效運作。但邊際處理在堅實的軟體工程中至關重要。所以,除非您真的走投無路,否則就強制為 Short 型別吧。在實踐中,為遠端伺服器上具有較大 ID 的物件執行此操作的一種方法是,在本地資料庫或 content provider 中定義一個金鑰,並使用該金鑰生成您的jobId。


您的整個應用只應該有一個 JobIdManager 類。該類可以為應用的所有作業生成 jobId:無論這些工作是否與頻道、使用者或者其他任何事情有關。事實上我們的示例 JobIdManager 類指出了這一點:並不是所有 JOB_TYPE 都與 Channel 操作有關。一個作業型別與使用者偏好有關,一個與使用者行為有關。JobIdManager 通過為每個作業型別分配一個不同的字首來覆蓋以上種型別。


您的應用中的每個 -JobService,都必須擁有唯一和最終的 JOB_TYPE_ 字首。再強調一次,必須是徹底的一對一關係


使用 JobIdManager

玩轉全新的 Android 8.0 Oreo 後臺策略

以下程式碼片段摘自 ChannelProgramsJobService,它為我們演示瞭如何在您的專案中使用 JobIdManager。無論何時需要安排新作業,都會使用 JobIdManager.getJobId(…) 生成 jobId。


import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.os.PersistableBundle;

public class ChannelProgramsJobService extends JobService {
  
   private static final String CHANNEL_ID = "channelId";
   . . .

   public static void schedulePeriodicJob(Context context,
                                      final int channelId,
                                      String channelName,
                                      long intervalMillis,
                                      long flexMillis)
{
   JobInfo.Builder builder = scheduleJob(context, channelId);
   builder.setPeriodic(intervalMillis, flexMillis);

   JobScheduler scheduler = 
            (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
   if (JobScheduler.RESULT_SUCCESS != scheduler.schedule(builder.build())) {
       //todo what? log to server as analytics maybe?
       Log.d(TAG, "could not schedule program updates for channel " + channelName);
   }
}

private static JobInfo.Builder scheduleJob(Context context,final int channelId){
   ComponentName componentName =
           new ComponentName(context, ChannelProgramsJobService.class);
   final int jobId = JobIdManager
             .getJobId(JobIdManager.JOB_TYPE_CHANNEL_PROGRAMS, channelId);
   PersistableBundle bundle = new PersistableBundle();
   bundle.putInt(CHANNEL_ID, channelId);
   JobInfo.Builder builder = new JobInfo.Builder(jobId, componentName);
   builder.setPersisted(true);
   builder.setExtras(bundle);
   builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
   return builder;
}

   ...
}複製程式碼


相信看到這裡,您對如何針對不同的場景來設計後臺機制有了比較清晰的認識。但不管怎樣,從 Oreo 開始對後臺任務做出的種種限制都會對提升使用者體驗有著現實的意義,這也要求開發者們對自己的應用需要完成以及何時需要完成一些事情有著更精準的規劃。如果您有什麼問題,或者經驗之談,歡迎在下面和我們分享哦~


* 注:感謝 Christopher Tate 和 Trevor Johnsz 在本文撰寫中提供的寶貴反饋意見

玩轉全新的 Android 8.0 Oreo 後臺策略



相關文章