咱們永遠都須要流暢的用戶體驗,但很遺憾咱們手上的硬件資源卻老是和這個需求唱反調。這也是 Android 平臺不斷努力的切入點——從 API 26開始,Android 對後臺服務引入了嚴格的限制。基本上,除非您的應用在前臺運行,不然系統將在幾分鐘內中止應用的全部後臺服務。
android
因爲對後臺服務的這些限制,JobScheduler 已經成爲執行後臺任務的實際解決方案。對於熟悉服務的開發者來講,JobScheduler 使用起來一般很簡單,固然也存在少許例外。咱們此次就來探討其中一個例外。數據庫
假如您正在搭建一個 Android TV 應用。頻道對電視應用很是重要,所以您的應用須要可以執行至少五種與頻道有關的後臺操做:發佈頻道,向頻道添加節目,將有關頻道的日誌發送到遠程服務器,更新頻道的元數據,以及刪除頻道。在 Android 8.0(Oreo)以前,這五個操做中的每個均可以在後臺服務中實現。然而,從 API 26 開始,您必須明智地決定,哪些應該沿用原有的普通後臺 Service,哪些應該使用 JobService。bash
若是隻考慮電視 App 的使用場景,上述五個操做裏,其實只有 「頻道發佈」 能夠作成一個原有的普通後臺服務。在某些場合下,頻道發佈涉及三個步驟:首先用戶單擊按鈕開始該過程; 而後,應用啓動後臺操做來建立和提交出版物; 最後,用戶經過用戶界面以確認訂閱。至此您能夠看到,發佈頻道須要用戶交互,所以須要可見的 Activity。因此,ChannelPublisherService 能夠是一個 IntentService,負責處理後臺邏輯。您不該該在這裏使用 JobService,由於 JobService 會引入延遲,而用戶交互一般須要您的應用進行即時響應。服務器
對於其餘四個操做,您應該使用 JobService; 由於它們均可以在您的應用位於後臺時執行。因此您應該分別建立 ChannelProgramsJobService,ChannelLoggerJobService,ChannelMetadataJobService,和 ChannelDeletionJobService。app
因爲以上全部的四個 JobService 都在處理 Channel 對象,您彷佛能夠方便地使用 channelId 做爲 jobId。可是因爲 JobService 在 Android Framework 中設計的方式,您不能這樣作。如下是 jobId 的官方描述:
ide
應用爲這個做業提供的 ID。 隨後調用取消,或建立相同 jobId 的做業,
將會更新已經存在的同一個 ID 的做業。該 ID 在同一個 uid 的全部客戶端(不僅是同一個應用包)中必須是惟一的。
您須要確保該 ID 在應用更新時始終保持穩定,所以它可能不該該基於資源 ID。 複製代碼
根據以上的描述,即便您使用 4 個不一樣的 Java 對象(即 -JobService),也仍然不能使用 channelId來做爲它們的 jobId。類級別的命名空間不能幫助到您。flex
這確實是個問題。您須要一個穩定、可擴展的方式來將 channelId 和它的 jobId 關聯起來。而最糟的結果莫過於,因爲 jobId 衝突,致使不一樣的頻道互相覆蓋操做。若是jobId 是 String 類型,而不是 Integer 類型的話,解決起來就很容易:ChannelProgramsJobService 的 jobId = "ChannelPrograms" + channelId, ChannelLoggerJobService 的 jobId = "ChannelLogs" + channelId,等等。但由於 jobId屬於 Integer 類型,而不屬於 String 類型,因此您就要設計一個智能的系統,用來爲您的做業生成可重複使用 jobId。ui
重點來了 —— 如今咱們來聊聊 JobIdManager,看怎樣用它來解決這個問題。spa
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_ 前綴。再強調一次,必須是完全的一對一關係。
如下代碼片斷摘自 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 在本文撰寫中提供的寶貴反饋意見