随着Android版本的不断更新,如何正确的处理后台任务变得愈来愈复杂。所以, Google发布了 WorkManager(做为JetPack的一部分)来帮助开发者解决这一难题。android
在学习WorkManager以前,首先得知道咱们为何须要它。本文将从如下三部分来阐述:git
Android系统的内核是基于Linux内核的,它与其余那些基于Linux内核的系统的主要差异在于Android系统没有交换空间(Swap space)。当系统内存资源已被耗尽,可是又有额外的内存资源请求的时候,内存中不活动的页面会被移动到交换空间。交换空间是磁盘上的一块区域,所以其访问速度比物理内存慢。ubuntu
鉴于此,Android系统引入了OOM( Out Of Memory ) Killer 来解决内存资源被耗尽的问题。它的做用是根据进程所消耗的内存大小以及进程的“visibility state”来决定是否杀死这个进程,从而达到释放内存的目的。bash
Activity Manager会给不一样状态下的进程设置相对应的oom_adj 值。下面是一些示例:服务器
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel. These are used in ActivityManagerService.
setprop ro.FOREGROUND_APP_ADJ 0 //前台进程
setprop ro.VISIBLE_APP_ADJ 1 //可见进程
setprop ro.SECONDARY_SERVER_ADJ 2 //次要服务
setprop ro.BACKUP_APP_ADJ 2 //备份进程
setprop ro.HOME_APP_ADJ 4 //桌面进程
setprop ro.HIDDEN_APP_MIN_ADJ 7 //后台进程
setprop ro.CONTENT_PROVIDER_ADJ 14 //内容供应节点
setprop ro.EMPTY_APP_ADJ 15 //空进程
复制代码
进程的omm_adj 值越大,它被 OOM killer 杀死的可能性越大。OOM killer是依据系统空闲的内存空间大小和omm_adj阈值的组合规则来杀死进程的。好比,当空闲的内存空间大小小于X1时,杀死那些omm_adj值大于Y1的进程。它的基本处理流程以下图所示:markdown
到如今为止,我但愿你知道两点:网络
A Service is an application component that can perform long-running operations in the background, and it does not provide a user interface.架构
使用service的理由以下:app
可是也存在一个缺点:我开发了本身的第一个应用,它居然在不到3个小时的时间内将电池电量从100%消耗至0%,由于个人应用开启了一个service:每3分钟从服务器中获取数据。ide
那时候我还只是一个年轻的没有经验的开发者。但不知为什么,6年以后,仍然有许多未知的应用程序在作着一样的事情。
每一位开发者能够毫无限制地在后台执行着任何他们想作的操做。google也意识到了这一点,并试图采起一些改进的措施。
从 Marshmallow 开始,而后是 Nougat , Android系统引入了休眠模式 (Doze mode)
何为休眠模式?简而言之,当用户关闭了手机屏幕以后,系统会自动进入休眠模式,禁止全部应用的网络请求、数据同步、GPS、闹钟、wifi扫描等功能,直到用户从新点亮屏幕或者手机接入了电源,这样能够有效节省手机的电量。
但这感受像是沧海一粟,所以从Android Oreo (API 26) 开始,Google 作了进一步改进:若是一个应用的目标版本为Android 8.0,当它在某些不被容许建立后台服务的场景下,调用了Service的startService()方法,该方法会抛出IllegalStateException。这个问题能够经过调整targeting SDK的值来解决,一些知名应用的target API都是22,由于他们不肯意处理运行时权限。
可是,接下来你会发现:
说了这么多 - (我相信你会得出一样的结论):
咱们所熟知的Servcie已经被弃用了,由于它再也不被容许在后台执行长时间的操做,而这倒是它最初被设计出来的目的。
除了Foreground service以外,咱们已经没有任何理由再去使用Service了。
首先举个简单的例子:有一个简单的网络请求,它能下载几千字节的数据。最简单的方法(并不正确的方法)就是开启一个单独的线程来执行这一请求。
int threads = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(threads);
executor.submit(myWork);
复制代码
再考虑下登陆场景。用户填写了邮箱、密码,而后点击了登陆按钮。用户的手机是3G网络,信号不好,接着他走进了电梯。
当应用正在执行登陆网络请求的时候,用户接了一个电话。
OkHttp 默认的超时时间很长
connectTimeout = 10_000;
readTimeout = 10_000;
writeTimeout = 10_000;
复制代码
一般咱们都会设置网络请求重试的次数为3
所以, 最坏的状况是: 3 * 30 = 90 秒.
如今请回答一个问题 ——
当你的应用退到后台以后,你就什么都不知道了。正如咱们所了解的,你不能期望你的应用进程会一直存活,以完成网络请求,处理响应并保存用户登陆信息。更不用说用户的手机还有可能进入离线模式,失去网络链接。
从用户的角度来看,我已经输入了个人邮箱和密码,而且点击了登陆按钮,所以我应该已经登陆成功了。假设没有登陆成功的话,用户会认为你的应用的用户体验不好,但事实上这并非用户体验的问题,而是一个技术问题。
接下来你就会思考,ok,一旦个人应用即将退到后台,我就开启Service去执行登陆操做,可是你不能!!!
这时候JobScheduler
就能派上用场了。
ComponentName service = new ComponentName(this, MyJobService.class);
JobScheduler mJobScheduler = (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(jobId, serviceComponent)
.setRequiredNetworkType(jobInfoNetworkType)
.setRequiresCharging(false)
.setRequiresDeviceIdle(false)
.setExtras(extras).build();
mJobScheduler.schedule(jobInfo);
复制代码
JobScheduler首先会调度一个任务,而后在合适的时机(好比说延迟若干时间以后,或者等手机空闲了)系统会开启你的MyJobService,而后执行onStartJob()里的处理逻辑。这个想法理论上很好,可是它只在API>21的系统上可用,并且在API 21&22的系统里JobScheduler还存在一个重大bug。
这意味着你只能在API>22的系统上使用JobScheduler。
若是你应用的minSDK < 23,你可使用JobDispatcher
。
Job myJob = firebaseJobDispatcher.newJobBuilder()
.setService(SmartService.class)
.setTag(SmartService.LOCATION_SMART_JOB)
.setReplaceCurrent(false)
.setConstraints(ON_ANY_NETWORK)
.build();
firebaseJobDispatcher.mustSchedule(myJob);
复制代码
等等, 它须要使用 Google Play Services!!
所以若是你打算使用JobDispatcher,你将会抛弃数千万用户。
所以,JobDispatcher 可能不是一个好的选择。那么AlarmManager呢?经过AlarmManager去轮询检查网络请求是否执行成功,若是没有的话尝试再次执行它?
若是你仍是想用Service来当即执行网络请求的话,能够选择JobIntentService
当SDK<26的时候,采用IntentService来执行任务;当SDK ≥ 26的时候,采用JobScheduler来执行任务。
Ahhhh… 它没法在 Android Oreo 上当即执行请求。
因此回到咱们开始的地方:当应用退到后台的时候,依据Android系统的版本和手机的状态,选择合适的任务调度器来调度执行后台任务。
天呐,要作到既能节省手机电池的的电量又能为用户提供惊艳的用户体验实在是太难了吧!
依据手机所处的状态、Android系统版本、手机是否拥有Google Play Services,能够选择对应的解决方案。你可能会尝试着本身去实现这一整套复杂的处理逻辑。好消息是Android framework的设计者已经听到了咱们的抱怨,他们决定去解决这个问题。
On the last Google I/O Android framework, the team announced WorkManager:
WorkManager aims to simplify the developer experience by providing a first-class API for system-driven background processing. It is intended for background jobs that should run even if the app is no longer in the foreground. Where possible, it uses JobScheduler or Firebase JobDispatcher to do the work; if your app is in the foreground, it will even try to do the work directly in your process.
哇! 这正是咱们须要的!
WorkManager库包含如下几个组件:
WorkManager
接收带参数和约束条件的WorkRequest,并将其排入队列。
Worker
你只须要实现doWork() 这一个方法,它是执行在一个单独的后台线程里的。全部须要在后台执行的任务都在这个方法里完成。
WorkRequest
给Worker设置参数和约束条件(好比,是否联网、是否接通电源)等。
WorkResult
Success, Failure, Retry.
Data
传递给Worker的持久化的键值对。
首先新建一个继承了Worker的类,并实现它的 doWork()方法:
public class LocationUploadWorker extends Worker {
...
//Upload last passed location to the server
public WorkerResult doWork() {
ServerReport serverReport = new ServerReport(getInputData().getDouble(LOCATION_LONG, 0),
getInputData().getDouble(LOCATION_LAT, 0), getInputData().getLong(LOCATION_TIME,
0));
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef =
database.getReference("WorkerReport v" + android.os.Build.VERSION.SDK_INT);
myRef.push().setValue(serverReport);
return WorkerResult.SUCCESS;
}
}
复制代码
而后使用WorkManager将它排入任务队列:
Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType
.CONNECTED).build();
Data inputData = new Data.Builder()
.putDouble(LocationUploadWorker.LOCATION_LAT, location.getLatitude())
.putDouble(LocationUploadWorker.LOCATION_LONG, location.getLongitude())
.putLong(LocationUploadWorker.LOCATION_TIME, location.getTime())
.build();
OneTimeWorkRequest uploadWork = new OneTimeWorkRequest.Builder(LocationUploadWorker.class)
.setConstraints(constraints).setInputData(inputData).build();
WorkManager.getInstance().enqueue(uploadWork);
复制代码
接下来,WorkManager将会合理地调度执行你的任务;它会存储任务全部的参数,任务的细节,更新任务的状态。你甚至可使用LiveData来订阅观察它的状态变化:
WorkManager.getInstance().getStatusById(locationWork.getId()).observe(this,
workStatus -> {
if(workStatus!=null && workStatus.getState().isFinished()){
...
}
});
复制代码
WorkManager库的架构图以下所示:
它能作的还不止这些。
你能够经过它来执行定时任务:
Constraints constraints = new Constraints.Builder().setRequiredNetworkType
(NetworkType.CONNECTED).build();
PeriodicWorkRequest locationWork = new PeriodicWorkRequest.Builder(LocationWork
.class, 15, TimeUnit.MINUTES).addTag(LocationWork.TAG)
.setConstraints(constraints).build();
WorkManager.getInstance().enqueue(locationWork);
复制代码
你也可让多个任务按顺序执行:
WorkManager.getInstance(this)
.beginWith(Work.from(LocationWork.class))
.then(Work.from(LocationUploadWorker.class))
.enqueue();
复制代码
你还可让多个任务同时执行:
WorkManager.getInstance(this).enqueue(Work.from(LocationWork.class,
LocationUploadWorker.class));
复制代码
固然你也能够将以上三种任务执行方式结合起来使用。
注意: 你不能构建一个将定时任务和一次性任务混合在一块儿的任务链。
WorkManager能够作不少事情: 取消任务, 组合任务, 构建任务链, 将一个任务的参数合并到另外一个任务。我建议你去查阅官方文档,里面有许多好的例子。
为了遵循节省用户手机电池电量的原则,Android每个版本都在不断改进,处理后台任务变得十分复杂。感谢Android团队,如今咱们可使用WorkManager来更加简单直接地处理处理后台任务。