
我们正在使用的iFunny应用程序已经在商店中使用了五年以上。 在这段时间里,移动团队不得不经历许多不同的方法以及工具之间的迁移,而一年前,有一段时间需要从自写解决方案切换到更“时尚”和更广泛的方向。 本文只是对研究的内容,研究的解决方案以及最终的解决方案进行了小幅挤压。
为什么我们需要所有这些?为了纪念本文的内容以及为什么这个主题对Android开发团队非常重要,让我们立即做出决定:
- 在许多情况下,您需要在活动用户界面框架之外运行任务。
- 系统对此类任务的启动施加了很多限制;
- 事实证明,在每种现有解决方案之间进行选择非常困难,因为每种工具都有其优缺点。
事件发展的年代
Android 0
AlarmManager,处理程序,服务
最初,实施他们的解决方案是为了基于服务启动基于后台的任务。 还有一种机制可以将任务链接到生命周期,并可以取消和恢复它们。 这很适合团队使用很长时间,因为该平台没有对此类任务施加任何限制。
Google建议根据以下图表进行操作:

到2018年底,了解这一点是没有意义的,足以评估灾难的规模。
实际上,没有人关心后台发生了多少工作。 应用程序完成了他们想要的工作,并在需要时完成了工作。
优点 :
随处可见;
所有人都可以使用。
缺点 :
系统以各种方式限制工作;
没有条件发射;
该API最少,您需要编写大量代码。
Android 5.棒棒糖
Jobcheduler
在接近2015年的5(!)年之后,Google注意到任务执行效率低下。 用户开始经常抱怨说,他们的手机只是躺在桌子上或放在口袋里就没电了。
随着Android 5的发布,出现了类似JobScheduler的工具。 这是一种可以在后台进行各种工作的机制,由于启动这些任务的集中系统以及为启动该任务设定条件的能力,该机制的开始得到了优化和简化。
在代码中,所有这些看起来都非常简单:宣布了其中包含开始和结束事件的服务。
从细微差别:如果要异步执行工作,则需要从onStartJob启动流; 最主要的是不要忘记在工作结束时调用jobFinished方法,否则系统不会放开WakeLock,您的任务将不会被视为已完成并且会丢失。
public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { doWork(params); return false; } @Override public boolean onStopJob(JobParameters params) { return false; } }
您可以从应用程序中的任何位置启动此工作。 任务在我们的流程中执行,但在IPC级别启动。 有一个集中的机制来控制它们的执行,并仅在必要时才唤醒应用程序。 您还可以设置各种触发条件,并通过捆绑包传输数据。
JobInfo task = new JobInfo.Builder(JOB_ID, serviceName) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .build(); JobScheduler scheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE); scheduler.schedule(task);
总的来说,相比什么都没有,这已经是什么了。 但是,该机制仅在API 21中可用,并且在Android 5.0发行时,停止支持所有旧设备(已经过去了3年,并且我们仍然支持Fours)会很奇怪。
优点 :
API很简单;
发射条件。
缺点 :
从API 21开始可用实际上,仅使用API 23;
容易犯错误。
Android 5.棒棒糖
GCM网络经理
还介绍了JobScheduler的类似物-GCM网络管理器。 这是一个提供相似功能的库,但已经与API 9一起使用。确实,它需要Google Play服务作为回报。 显然,JobScheduler工作所需的功能不仅开始通过Android版本提供,而且还开始在GPS级别提供。 应当指出,该框架的开发人员很快改变了主意,并决定不将他们的未来与GPS连接起来。 多亏了他们。
一切看起来都完全相同。 相同的服务:
public class GcmNetworkManagerService extends GcmTaskService { @Override public int onRunTask(TaskParams taskParams) { doWork(taskParams); return 0; } }
相同的任务启动:
OneoffTask task = new OneoffTask.Builder() .setService(GcmNetworkManagerService.class) .setTag(TAG) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresCharging(true) .build(); GcmNetworkManager mGcmNetworkManager = GcmNetworkManager.getInstance(this); mGcmNetworkManager.schedule(task);
架构的相似性是由继承的功能和在工具之间进行简单迁移的愿望所决定的。
优点 :
与JobScheduler类似的API;
从API 9开始可用。
缺点 :
您必须拥有Google Play服务
容易犯错误。Android 5.棒棒糖
唤醒广播接收器
接下来,我将简要介绍JobScheduler中使用的一种基本机制,开发人员可以直接使用。 这是WakeLock及其基础的WakefulBroadcastReceiver。
使用WakeLock,可以防止系统进入挂起状态,即保持设备处于活动状态。 如果我们想做一些重要的工作,这是必要的。
创建WakeLock时,可以指定其设置:按住CPU,屏幕或键盘。
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE) PowerManager.WakeLock wl = pm.newWakeLock(PARTIAL_WAKE_LOCK, "name") wl.acquire(timeout);
基于此机制,WakefulBroadcastReceiver可以工作。 我们启动服务并按住WakeLock。
public class SimpleWakefulReceiver extends WakefulBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent service = new Intent(context, SimpleWakefulService.class); startWakefulService(context, service); } }
服务完成必要的工作后,我们通过类似的方法将其释放。
通过4个版本,此BroadcastReceiver将被弃用,并在developer.android.com上介绍以下替代方案:
- JobScheduler;
- 协同剑
- 下载管理器
- 用于窗口的FLAG_KEEP_SCREEN_ON。
Android 6.棉花糖
DozeMode:随时随地睡眠
然后Google开始对设备上运行的应用程序进行各种优化。 但是,对用户而言最优化的是对开发人员的限制。
第一步是DozeMode,如果设备闲置了一段时间,它将进入睡眠模式。 在第一个版本中,它持续一个小时,在后续版本中,睡眠时间减少到30分钟。 手机会定期唤醒,执行所有待处理的任务,然后再次入睡。 DozeMode窗口以指数形式展开。 可以通过adb跟踪模式之间的所有转换。
当发生DozeMode时,会对应用程序施加以下限制:
- 系统将忽略所有WakeLock;
- AlarmManager延迟;
- JobScheduler无法正常工作;
- SyncAdapter无法正常工作;
- 网络访问受到限制。
您也可以将应用程序添加到白名单中,这样它就不会受到DozeMode的限制,但是至少三星完全忽略了此列表。
Android 6.棉花糖
AppStandby:不活动的应用程序
系统识别不活动的应用程序,并对它们施加与DozeMode中相同的限制。
在以下情况下,会将应用程序发送到隔离:
- 在前台没有进程;
- 没有有效的通知;
- 未添加到排除列表。
Android 7.牛轧糖
背景优化。 斯维尔特
Svelte是一个项目,其中Google试图优化应用程序和系统本身的RAM消耗。
在Android 7中,在该项目的框架内,已确定隐式广播不是很有效,因为它们被大量应用程序监听,并且当这些事件发生时,系统会花费大量资源。 因此,清单中禁止声明以下类型的事件:
- CONNECTIVITY_ACTION;
- ACTION_NEW_PICTURE;
- ACTION_NEW_VIDEO。
Android 7.牛轧糖
FirebaseJobDispatcher
同时,发布了任务启动框架的新版本-FirebaseJobDispatcher。 实际上,这是完整的GCM NetworkManager,它经过整理和灵活一些。
在视觉上,一切看起来都完全一样。 相同的服务:
public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { doWork(params); return false; } @Override public boolean onStopJob(JobParameters params) { return false; } }
他之间的唯一区别是安装驱动程序的能力。 驱动程序是负责任务启动策略的类。
任务的启动本身并没有随着时间而改变。
FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); Job task = dispatcher.newJobBuilder() .setService(FirebaseJobDispatcherService.class) .setTag(TAG) .setConstraints(Constraint.ON_UNMETERED_NETWORK, Constraint.DEVICE_IDLE) .build(); dispatcher.mustSchedule(task);
优点 :
与JobScheduler类似的API;
从API 9开始可用。
缺点 :
您必须拥有Google Play服务
容易犯错误。
安装我的驱动程序摆脱GPS的做法令人鼓舞。 我们甚至进行了搜索,但最终找到了以下内容:


Google知道这一点,但是这些任务仍然存在数年。
Android 7.牛轧糖
Evernote的Android Job
结果,社区无法忍受,一个自制的解决方案以Evernote的图书馆形式出现。 它不仅是唯一的解决方案,而且还是Evernote的解决方案,它能够建立自己的地位并“融入人民”。
从体系结构的角度来看,该库比其前身更方便。
出现了负责创建任务的实体。 对于JobScheduler,它们是通过反射创建的。
class SendLogsJobCreator : JobCreator { override fun create(tag: String): Job? { when (tag) { SendLogsJob.TAG -> return SendLogsJob() } return null } }
有一个单独的类,即任务本身。 在JobScheduler中,所有这些都被转储到onStartJob内部的开关中。
class SendLogsJob : Job() { override fun onRunJob(params: Params): Result { return doWork(params) } }
任务的启动是相同的,但Evernote除了继承的事件外,还添加了自己的任务,例如启动日常任务,唯一任务以及在窗口内启动。
new JobRequest.Builder(JOB_ID) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .build() .scheduleAsync();
优点 :
方便的API;
所有版本均受支持;
不需要Google Play服务。
缺点 :
第三方解决方案。
伙计们积极支持他们的图书馆。 尽管存在很多关键问题,但它适用于所有版本和所有设备。 结果,去年我们的Android团队选择了Evernote的解决方案,因为Google的库削减了他们无法支持的大量设备。
在内部,她在极端情况下使用AlarmManager研究Google的解决方案。
Android 8. Oreo
后台执行限制
让我们回到我们的局限性。 随着新的Android的到来,新的优化方法已经出现。 谷歌的家伙发现了另一个问题。 这次,事实证明整个过程都在服务和广播中(是的,没有新内容)。
如果后台有应用程序,则为startService清单中的隐式广播
首先,禁止从后台启动服务。 在“法律框架”中,仅保留前台服务。 现在可以说服务已过时。
第二个限制是相同的广播。 这次禁止在清单中注册所有隐式广播。 隐式广播是一种广播,不仅仅用于我们的应用。 例如,有动作ACTION_PACKAGE_REPLACED,有ACTION_MY_PACKAGE_REPLACED。 因此,第一个是隐式的。
但是,仍然可以通过Context.registerBroadcast注册任何广播。
Android 9. Pie
工作经理
关于此优化尚未停止。 就能耗而言,这些设备也许开始迅速而谨慎地工作; 也许用户对此抱怨较少。
在Android 9中,该框架的开发人员彻底使用了该工具来启动任务。 为了解决所有紧迫的问题,在Google I / O上引入了一个库,用于启动WorkManager的后台任务。
Google最近一直在尝试塑造其对Android应用程序体系结构的愿景,并为开发人员提供为此所需的工具。 因此,存在带有LiveData,ViewModel和Room的建筑组件。 WorkManager看起来是对其方法和范例的合理补充。
如果我们谈论WorkManager的内部布置方式,则其中没有技术突破。 实际上,这是现有解决方案的包装:JobScheduler,FirebaseJobDispatcher和AlarmManager。
createBestAvailableBackgroundScheduler static Scheduler createBestAvailableBackgroundScheduler(Context, WorkManager) { if (Build.VERSION.SDK_INT >= MIN_JOB_SCHEDULER_API_LEVEL) { return new SystemJobScheduler(context, workManager); } try { return tryCreateFirebaseJobScheduler(context); } catch (Exception e) { return new SystemAlarmScheduler(context); } }
选择代码非常简单。 但应注意的是,JobScheduler从API 21开始可用,但由于第一个版本相当不稳定,因此它们仅与API 23一起使用。
如果版本低于23,则通过反射尝试找到FirebaseJobDispatcher,否则使用AlarmManager。
值得注意的是,包装器的包装非常灵活。 这次,开发人员将所有内容分解为单独的实体,在结构上看起来很方便:
- 工人-工作逻辑;
- WorkRequest-任务启动的逻辑;
- WorkRequest.Builder-参数;
- 约束-条件;
- WorkManager-管理任务的经理;
- WorkStatus-任务状态。

启动条件是从JobScheduler继承的。
可以注意到,用于更改URI的触发器仅在API 23中出现。此外,使用方法中的标志,您不仅可以订阅特定URI的更改,还可以订阅所有嵌套在其中的更改。
如果我们谈论我们,那么在Alpha阶段,决定切换到WorkManager。
这有几个原因。 Evernote有几个关键错误,该库的开发人员承诺在过渡到集成WorkManager的版本时会修复这些错误。 他们自己也同意Google的决定否定了Evernote的优势。 另外,由于我们使用架构组件,因此该解决方案非常适合我们的架构。
此外,我想以一个简单的示例说明我们如何尝试使用这种方法。 同时,是否拥有WorkManager或JobScheduler并不是很关键。


让我们来看一个非常简单的例子:单击重新发布或点赞。
现在,所有应用程序都试图摆脱阻塞对网络的请求,因为这使用户感到紧张并使其等待,尽管此时他可以在应用程序内进行购买或观看广告。
在这种情况下,本地数据首先会发生变化-用户立即看到其操作的结果。 然后在后台向服务器发送请求,如果请求失败,则数据将重置为其初始状态。
接下来,我将展示一个示例,说明我们的情况。
JobRunner包含启动任务的逻辑。 他的方法描述了任务的配置和传递参数。
JobRunner.java fun likePost(content: IFunnyContent) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val input = Data.Builder() .putString(LikeContentJob.ID, content.id) .build() val request = OneTimeWorkRequest.Builder(LikeContentJob::class.java) .setInputData(input) .setConstraints(constraints) .build() WorkManager.getInstance().enqueue(request) }
WorkManager中的任务本身如下:我们从参数中获取ID,并在服务器上调用方法以喜欢此内容。
我们有一个包含以下逻辑的基类:
abstract class BaseJob : Worker() { final override fun doWork(): Result { val workerInjector = WorkerInjectorProvider.injector() workerInjector.inject(this) return performJob(inputData) } abstract fun performJob(params: Data): Result }
首先,它可以使您远离对Worker的明确了解。 它还通过WorkerInjector包含依赖项注入逻辑。
WorkerInjectorImpl.java @Singleton public class WorkerInjectorImpl implements WorkerInjector { @Inject public WorkerInjectorImpl() {} @Ovierride public void inject(Worker job) { if (worker instanceof AppCrashedEventSendJob) { Injector.getAppComponent().inject((AppCrashedEventSendJob) job); } else if (worker instanceof CheckNativeCrashesJob) { Injector.getAppComponent().inject((CheckNativeCrashesJob) job); } } }
它只是代理对Dagger的调用,但可以帮助我们进行测试:我们替换注射器的实现,并在任务中实现必要的环境。
fun void testRegisterPushProvider() { WorkManagerTestInitHelper.initializeTestWorkManager(context) val testDriver = WorkManagerTestInitHelper.getTestDriver() WorkerInjectorProvider.setInjector(TestInjector())
class LikePostInteractor @Inject constructor( val iFunnyContentDao: IFunnyContentDao, val jobRunner: JobRunner) : Interactor { fun execute() { iFunnyContentDao.like(getContent().id) jobRunner.likePost(getContent()) } }
交互器是ViewController拉动以启动脚本通过的实体(在这种情况下,就像它一样)。 我们在本地将内容标记为“已上传”,然后提交任务以执行。 如果任务失败,将删除类似内容。
class IFunnyContentViewModel(val iFunnyContentDao: IFunnyContentDao) : ViewModel() { val likeState = MediatorLiveData<Boolean>() var iFunnyContentId = MutableLiveData<String>() private var iFunnyContentState: LiveData<IFunnyContent> = attachLiveDataToContentId(); init { likeState.addSource(iFunnyContentState) { likeState.postValue(it!!.hasLike) } } }
我们使用Google的架构组件:ViewModel和LiveData。 这就是我们的ViewModel的样子。 在这里,我们将状态为like的DAO中的对象更新连接起来。
IFunnyContentViewController.java class IFunnyContentViewController @Inject constructor( private val likePostInteractor: LikePostInteractor, val viewModel: IFunnyContentViewModel) : ViewController { override fun attach(view: View) { viewModel.likeState.observe(lifecycleOwner, { updateLikeView(it!!) }) } fun onLikePost() { likePostInteractor.setContent(getContent()) likePostInteractor.execute() } }
一方面,ViewController订阅更改状态的请求,另一方面,它启动我们需要的脚本的传递。
这几乎是我们需要的所有代码。 剩下的就是添加View本身的行为以及DAO的实现; 如果使用“房间”,则只需在对象中注册字段。 它看起来非常简单有效。
总结一下
JobScheduler,GCM网络经理,FirebaseJobDispatcher:- 不要使用它们
- 不再阅读有关它们的文章
- 不看报告
- 不要以为选择哪一个。
Evernote的Android Job:- 在内部,他们将使用WorkManager;
- 关键错误在解决方案之间是模糊的。
工作管理器:- API等级9以上;
- 独立于Google Play服务;
- 链接/输入合并器;
- 反应方法
- Google的支持(我想相信)。