作业调度器与后台服务

48
我有一个应用程序,其中有一个功能A,应该每分钟在后台运行。功能A是应用程序应连接到数据库,读取一些数据,然后获取设备的当前位置,并根据它们检查条件,如果条件成立,它应向用户发送状态栏通知,以便当用户单击通知时,应用程序的UI将显示并发生某些事情。
此后台任务应每分钟永久运行,无论应用程序是否被使用、关闭、终止(如Facebook或WhatsApp,它们会向我们显示通知,无论它们是否在应用程序堆栈中)。
现在我已经搜索并发现Android提供了作业调度程序后台服务AlarmManager处理程序
但是,我阅读的越多,就越发现这些陈述之间的矛盾。
  1. 关于处理程序,我已经了解到它们不适用于长时间延迟并且会在系统重启后终止。所以它们不适合我的任务。
  2. 但是 AlarmManager 似乎是这个问题的一个好选择,因为当允许时,它们甚至可以在系统重启后继续存在并重新运行应用程序。但是在 Android Documentation 中指出,AlarmManager 旨在用于必须在特定时间运行的任务(如闹钟)。但是我的任务必须每分钟运行。
  3. 然后是后台服务。据我所知,这更适用于像后台下载之类的任务,并不适用于执行我所解释的任务。
  4. JobScheduler 似乎不适用于需要永久完成的任务,而是用于满足特定约束条件的任务,例如空闲或无网络...那么在这些选项中(如果有其他选项),您建议使用哪一个来完成我在第一部分中解释的任务?

你说过你需要从数据库中获取一些数据,然后检查一个条件。 最好的方法是从服务器推送信息。 - Erick M. Sprengel
最好的方法是使用GCM从服务器推送数据。但这需要服务器改进(也许您无法对服务器进行更改)。 (我知道这不是问题,但它可能会帮助那些以错误的方式开始寻找应用程序更新的人。) - Erick M. Sprengel
@ErickM.Sprengel,服务器推送是可靠地通过Firebase Messaging(之前为GCM)传递的吗? - user2297550
7个回答

36
我有一个应用程序,其中具有每分钟在后台运行的功能A。
对于那些运行Android 6.0及更高版本的数亿个Android设备,由于Doze模式(以及可能取决于应用程序的其余部分的应用待机),这是不可能的。
但似乎AlarmManager是解决问题的好选择,因为如果允许,则可以在系统重启后继续存在。
不,它们不会。您需要在重启后重新安排所有使用AlarmManager预定的警报。
Alarm Manager旨在用于必须在特定时间运行的任务。
AlarmManager支持重复选项。
这更适用于像我所读到的后台下载之类的任务,并非用���执行我已经解释过的操作。
无论使用什么解决方案,都需要Service。
JobScheduler似乎不适用于必须永久完成的任务,而适用于满足特定约束条件的任务,如空闲或没有网络。
与AlarmManager一样,JobScheduler支持重复作业。
所以你推荐使用哪一个(或其他的,如果有的话)来完成我在第一部分中解释的任务?
不要使用任何方法,因为一旦设备进入Doze模式,您无法在Android 6.0+上每分钟运行任务,而这将在屏幕关闭后的一小时内发生。相反,重新设计应用程序,使其只需要在后台工作几次,或者不要费心编写应用程序。

8
如果您提到Facebook和WhatsApp,它们很可能使用Firebase Cloud Messaging。这是将消息推送到设备的服务器,用于在服务器上发生相关数据更改时。即使在Doze模式下,FCM消息也允许应用程序临时执行一些操作。如果通过“连接到数据库”指的是轮询某个Web服务,则可以使用FCM切换到推送模型,而不是尝试在设备每分钟执行某些操作。 - CommonsWare
2
不写应用程序的最后选择是夸张其词。虽然Doze模式会在设备休眠时停止后台服务,但如果您的应用程序绝对需要它,则仍然可以指示用户并将其带到设置页面以禁用“电池优化”设置,从而防止绕过Doze模式影响您的应用程序的后台服务。 - Cord Rehn
2
@user2297550:“Facebook和WhatsApp的可靠性和即时性是否自动免费与FCM一起提供?” - 我会使用FCM进行机会交付,并定期(不可靠)轮询作为备份。 “可以推断出FCM在Android + Google中具有其他消息提供程序永远无法拥有的特殊地位吗?” - 如果您所说的“Android + Google”是指“Google Play生态系统”,那么Google的巨大优势在于他们的东西是预安装的,预添加到电池优化白名单中,并且不能被Google禁止。 - CommonsWare
1
好的,假设IFTTT使用FCM。那么它如何仅通过FCM处理地理位置业务逻辑?如果FCM可以完全处理这种业务逻辑,那么你不认为你的答案是夸张和误导性的吗?相反,你应该指导OP使用FCM而不是取消他们的应用程序并计划不再构建它。 - tshm001
1
IFTTT绝对每分钟都在工作,最少2次。一旦我离开我的小区地理围栏,它通常会立即触发并在很短的时间内关闭我的EcoBee。可能是1到2分钟,但它并不像你所暗示的那样不可能。 - tshm001
显示剩余12条评论

15

如果你的minSdkVersion=21,你可以使用在Android 5.0中引入的现代JobScheduler API。

此外,还有一个需要安装Google Play且minSdkVersion=9的https://github.com/firebase/firebase-jobdispatcher-android

但我建议使用这个库https://github.com/evernote/android-job,它会根据Android版本使用JobSchedulerGcmNetworkManagerAlarmManager其中之一。

使用这些API,您可以调度作业并运行描述任务的服务。

更新 现在最好使用新的WorkManager(文档)。 android-job即将过时。


43
该公司废弃API的速度比更新文档的速度还快。我花了很多时间来追寻何种后台处理方式更适合我使用:是Service?不是!AlarmManager?也不是!AsyncTask?ThreadPool、Bound Service?BroadcastReceiver?JobScheduler?Firebase JobScheduler?WorkManager?FCM?目前所有文档都建议使用Firebase-JobScheduler。但现在却已经改为使用可能会使用Firebase或JobScheduler的WorkManager了。天啊! - Samuel

7

首先,JobService是一种服务。后台服务有歧义,让我猜猜你是指在后台线程中运行的服务。Job Service在UI线程上运行,但您可以在其中创建一个异步任务对象,以使其在后台运行。

从你的问题来看,JobService不是最合适的选择。我建议使用:

  1. You can create a class that extends IntentService (this runs on the background thread) in the onDestroy method of that class, send a broadcast and make the broadcast restart the service.

     @onDestroy(){
     Intent broadcastIntent = new 
     Intent("com.example.myapp.serviceRestarted");
     sendBroadcast(broadcastIntent);}
    
  2. Create a class that extends broadcast reciever

     public class RestartServiceReceiver extends BroadcastReceiver {
     @Override
     public void onReceive(Context context, Intent intent) {
     context.startService(new Intent(context, 
     MyService.class));
    } 
    }
    
    1. In your manifest, register your service and reciever
<receiver
            android:name=".RestartServiceReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.myapp.serviceRestarted" />
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

引导权限是为了在系统完成启动后使接收器被调用,一旦接收器被调用,服务将再次被调用。


3
在Lollipop及以上版本中(即API版本21),您可以使用JobScheduler来安排JobService。为了每分钟重复一个作业,您需要在每次作业完成后设置最小延迟为60*1000毫秒以安排作业。
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class MyJobService extends JobService {

    boolean isWorking = false;
    boolean jobCancelled = false;

    @Override
    public boolean onStartJob(JobParameters params) {
        Log.d("_____TAG_____", "MyJobService started!");
        isWorking = true;

        doWork(params);

        return isWorking;
    }

    private void doWork(JobParameters params) {

        if (jobCancelled)
            return;

        //Create a new thread here and do your work in it. 
        //Remember, job service runs in main thread

        Log.d("_____TAG_____", "MyJobService finished!");
        isWorking = false;
        boolean needsReschedule = false;
        jobFinished(params, needsReschedule);

        scheduleRefresh();

    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.d("_____TAG_____", "MyJobService cancelled before being completed.");
        jobCancelled = true;
        boolean needsReschedule = isWorking;
        jobFinished(params, needsReschedule);
        return needsReschedule;
    }

    private void scheduleRefresh() {

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            ComponentName componentName = new ComponentName(getApplicationContext(), MyJobService.class);
            JobInfo.Builder builder = new JobInfo.Builder(5, componentName);
            builder.setMinimumLatency(60*1000);  //1 minute
            JobInfo jobInfo = builder.build();

            JobScheduler jobScheduler = (JobScheduler)getApplicationContext().getSystemService(JOB_SCHEDULER_SERVICE);
            int resultCode = jobScheduler.schedule(jobInfo);
            if (resultCode == JobScheduler.RESULT_SUCCESS) {
                Log.d("_____TAG_____", "MyJobService scheduled!");
            } else {
                Log.d("_____TAG_____", "MyJobService not scheduled");
            }
        }
    }

}

您可以编写一个公共函数,在任何您喜欢的地方,用于首次调度作业-
public void scheduleMyJobService() {

    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
        ComponentName componentName = new ComponentName(context, MyJobService.class);
        JobInfo.Builder builder = new JobInfo.Builder(5, componentName);
        builder.setMinimumLatency(60*1000);
        JobInfo jobInfo = builder.build();

        JobScheduler jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE);
        int resultCode = jobScheduler.schedule(jobInfo);
        if (resultCode == JobScheduler.RESULT_SUCCESS) {
            Log.d("_____TAG_____", "MyJobService scheduled!");
        } else {
            Log.d("_____TAG_____", "MyJobService not scheduled");
        }
    }
}

在我被踩之前,请给我一个公正的解释,因为它完全符合 OP 的要求:即使应用程序被杀死,每分钟执行一次操作。 - Chirag Mittal
您还需要注册服务: <service android:name="com.example.appname.service.MyJobService" android:enabled="true" android:label="十分钟服务" android:permission="android.permission.BIND_JOB_SERVICE" /> - Ammar

2

1
链接2: https://developer.android.com/topic/performance/scheduling.html - Mena
推荐上面的BigNedRanch链接,它很好地涵盖了这个问题。基本上使用JobScheduler,不要考虑API < 21,因为它只占市场的10%并且正在下降。不过我想知道为什么文章没有提到Service类呢? - Nick T

1
在之前的Android版本中,人们使用Handler或后台服务来实现此目的。不久之后,他们宣布了Alarm Manager类用于永久性、定期的工作。
Whatsapp、Facebook或一些社交媒体应用程序通常使用Google云消息传递进行通知,但这对你来说并不有用。
我建议您使用Alarm Manager。在KitKat版本(4.2)之后,操作系统会阻止后台处理程序以更好地使用电池。
后台服务通常用于图像上传或某些具有结束时间的重型进程。当您向Whatsapp上的朋友发送视频时,后台进程启动并将视频上传到后端服务器。

我不确定JobScheduler API是否支持旧版支持,但它与Alarm Manager一样好。

0

你可以使用服务来实现,使用返回值start_sticky。"START_STICKY告诉操作系统在有足够内存时重新创建服务,并使用空意图再次调用onStartCommand()。START_NOT_STICKY告诉操作系统不要再重新创建服务。还有第三个代码START_REDELIVER_INTENT,它告诉操作系统重新创建服务并将相同的意图传递给onStartCommand()"

同时,设置一个周期为1分钟的计时器并执行你的代码。

如果你想在用户强制停止服务后重新启动服务,你可以像之前的回答一样做到这一点。

  1. 您可以创建一个扩展IntentService的类(这在后台线程上运行),在该类的onDestroy方法中,发送广播并使广播重新启动服务。

    @onDestroy(){
        Intent broadcastIntent = new Intent("com.example.myapp.serviceRestarted");
        sendBroadcast(broadcastIntent);
    }
    
  2. 创建一个扩展广播接收器的类

    public class RestartServiceReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            context.startService(new Intent(context, MyService.class));
        } 
    }
    
  3. 在您的清单文件中,注册您的服务和接收器

    <receiver
       android:name=".RestartServiceReceiver"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="com.example.myapp.serviceRestarted" />
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
    </receiver>
    

此外,您可以使用AlarmManager。如果需要在Doze模式下触发警报,请使用:

setAndAllowWhileIdle()或setExactAndAllowWhileIdle()。

将其设置为“当前时间(以秒为单位)+ 60秒”,这样您就会在下一分钟设置它。

执行您的代码,并在最后重置AlarmManager到下一分钟。

此外,您可以在设备重新启动后启动服务或AlarmManager,只需在“RECEIVE_BOOT_COMPLETED”时使用广播接收器即可。

并添加此权限:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

在较高的Android设备上,API 5或6以上,如果用户没有与应用程序交互且应用程序处于后台,则设备会经常进入待机状态,并且Android系统将终止后台服务。 - Kaveesh Kanwal
@Code Pope 的一个要点是,根据文档 https://developer.android.com/training/monitoring-device-state/doze-standby 当屏幕关闭、设备未插电并且 - 对于您的应用程序来说这是有趣的事情 - 设备静止时,设备会进入Doze模式。根据这个,尽管设备可能进入Doze模式,但对您来说可能无关紧要,因为位置不会改变(因此可能不需要通知)。另一方面,如果仅通过数据库中的更改就能触发通知,则这将无法工作。 - Lefteris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接