Android

Android实现定时任务的几种方式

2022-10-29  本文已影响0人  小城哇哇

相比Android倒计时的常用场景,定时任务相对来说使用的场景相对没那么多,除非一些特殊的设备或者一些特殊的场景我们会用到。

关于定时任务可以分为两种作用范围,App内部范围和App外部范围,也就是说你是否需要App杀死了还能执行定时任务,需求不同实现的方式也不同,我们来看看都如何实现。

一、App内部范围

其实App内部范围的定时任务,我们可以使用倒计时的方案,Handler天然就支持。其实我们并不是需要每一次都使用一些系统服务让App外部范围生效。

比如场景如下,机器放在公司前台常亮并且一直运行在前台的,我需要没间隔60秒去查询当前设备是否在线,顺便更新一下当前的时间,显示早上好,中午好,下午好。

这样的场景我不需要使用一些系统服务,使用App内部范围的一些定时任务即可,因为就算App崩溃了,就算有系统级别的定时任务,我App不在了也没有用了,所以使用内部范围的定时任务即可,杀鸡焉用牛刀。

之前的倒计时方案改造一番几乎都能实现这样的定时任务,例如:

    private var mThread: Thread = Thread(this)
    private var mflag = false
    private var mThreadNum = 60

    override fun run() {
        while (mflag && mThreadNum >= 0) {
            try {
                Thread.sleep(1000 * 60)
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }

            val message = Message.obtain()
            message.what = 1
            message.arg1 = mThreadNum
            handler.sendMessage(message)

            mThreadNum--
        }
    }

    private val handler = Handler(Looper.getMainLooper()) { msg ->

        if (msg.what == 1) {
            val num = msg.arg1
            //由于需要主线程显示UI,这里使用Handler通信
            YYLogUtils.w("当时计数:" + num)
        }

        true
    }

      //定时任务
      fun backgroundTask() {

            if (!mThread.isAlive) {

                mflag = true

                if (mThread.state == Thread.State.TERMINATED) {
                    mThread = Thread(this@DemoCountDwonActivity)
                    if (mThreadNum == -1) mThreadNum = 60
                    mThread.start()
                } else {
                    mThread.start()
                }
            } else {

                mflag = false

            }

        }

这样每60秒就能执行一次任务,并且不受到系统的限制,简单明了。(只能在App范围内使用)

倒计时的一些的一些方案我们都能改造为定时任务的逻辑,比如上面的Handler,还有Timer的方式,Thread的方式等。

除了倒计时的一些方案,我们额外的还能使用Java的线程池Api也能快速的实现定时任务,周期性的执行逻辑,例如:


val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(3)
val command = Runnable {
    //dosth
}
executorService.scheduleAtFixedRate(command, 0, 3, TimeUnit.SECONDS)

// executorService.shutdown() //如果想取消可以随时停止线程池

定时执行任务在平常的开发中相对不是那么多,比如特殊场景下,我们需要轮询请求。

比如我做过一款应用放在公司前台,我就需要轮询请求每180秒调用服务器接口,告诉服务器当前设备是否在线。便于后台统计,这个是当前App内部生命周期执行的,用在这里刚刚好。

又比如我们使用DownloadManager来下载文件,因为不能直接回调进度,需要我们手动的调用Query去查询当前下载的消息,和文件的总大小,计算当前的下载进度,我们就可以使用轮询的方案,每一秒钟调用一次Query获取进度,模拟了下载进度的展示。

二、App外部范围

有内部范围的定时任务了,那么哪一种情况下我们需要使用外部范围的定时任务呢?又如何使用外部范围的定时任务呢?

还是上面的场景,机器放在公司前台常亮并且一直运行在前台的,这个App我们需要自动更新,并且检查是否崩溃了或者在前台,不管App是否存在我们都需要自行的定时任务,超过App的生命周期了,我们需要使用系统服务的定时任务来做这些事情。

都有哪些系统服务可以完成这样的功能呢?

2.1 系统服务的简单对比与原理

AlarmManager JobSchedule WorkManager !三者又有哪些不同呢?

AlarmManager 和 JobSchedule 虽然都是系统服务,但是方向又不同,AlarmManager 是通过 AlarmManagerService 控制RTC芯片。

说起Alar就需要说到RTC,说到RTC就需要讲到WakeLock机制。

话说回来,AlarmManage有一个 AlarmManagerService ,该服务程序主要维护 app 注册下来的各类Alarm, 并且一直监听 Alarm 设备, 一旦有 Alarm 触发,或者是 Alarm 事件发生,AlarmManagerService 就会遍历 Alarm 列表,找到相应的注册 Alarm 并发出广播. 首先, Alarm 是基于 RTC 实时时钟计时, 而不是CPU计时; 其次, Alarm 会维持一个 CPU 的 wake lock, 确保 Alarm 广播能被处理。

他们之间的区别是,AlarmManager 最终是操作硬件,设备开机通电和关机就会丢失Alarm任务,而 JobSchedule 是系统级别的任务,就算重启设备也会继续执行。并且相较来说 AlarmManager 可以做到精准度可以比 JobSchedule 更加好点。

而 WorkManager 则是对JobSchedule的封装与兼容处理,6.0以上版本内部实现JobSchedule,一下的版本提供 AlarmManager 。提供的统一的Api实现相同的功能。

所以在2022年的今天,系统级别的定时任务就只推荐用 AlarmManager(短时间) 或者 WorkManager(长时间)了。

2.2 AlarmManager实现定时任务

由于各版本的不同使用的方式不同 API > 19的时候不能设置为循环 需要设置为单次的发送 然后在广播中再次设置单次的发送。

当API >23 当前手机版本为6.0的时候有待机模式的省点优化 需要重新设置。

当设备为Android 12,如果使用到了AlarmManager来设置定时任务,并且设置的是精准的闹钟(使用了setAlarmClock()、setExact()、setExactAndAllowWhileIdle()这几种方法),则需要确保SCHEDULE_EXACT_ALARM权限声明且打开,否则App将崩溃。

需要在AndroidManifest.xml清单文件中声明 SCHEDULE_EXACT_ALARM 权限

最终我们兼容所有的做法是,只开启一个定时任务,然后触发到广播,然后再广播中再次启动一个定时任务,依次循环。

例如我们设置一个 AlarmManager ,每180秒检查一下 App 是否存活,如果 App 不在了就拉起 App 跳转MainActivity。(需求是当App杀死了也能启动首页,所以不适用于App内的定时执行方案)。

    //定时任务
        fun backgroundTask() {

            //开启3分钟的闹钟广播服务,检测是否运行了首页,如果退出了应用,那么重启应用
            val alarmManager = applicationContext.getSystemService(ALARM_SERVICE) as AlarmManager

            val intent1 = Intent(CommUtils.getContext(), AlarmReceiver::class.java)
            val pendingIntent = PendingIntent.getBroadcast(CommUtils.getContext(), 0, intent1, PendingIntent.FLAG_UPDATE_CURRENT)

            //先取消一次
            alarmManager.cancel(pendingIntent)

            //再次启动,这里不延时,直接发送
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent)
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent)
            } else {
                alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), 18000, pendingIntent)
            }

            YYLogUtils.w("点击按钮-开启 alarmManager 定时任务啦")

        }

点击按钮就发送一个立即生效的闹钟,逻辑走到广播中,然后再广播中再次开启闹钟。

class AlarmReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent?) {

        val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager

        //执行的任务
        val intent1 = Intent(CommUtils.getContext(), AlarmReceiver::class.java)
        val pendingIntent: PendingIntent = PendingIntent.getBroadcast(context, 0, intent1, PendingIntent.FLAG_UPDATE_CURRENT)

        // 重复定时任务,延时180秒发送
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 180000, pendingIntent)
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 180000, pendingIntent)
        } else {
            alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), 180000, pendingIntent);
        }

        YYLogUtils.w("AlarmReceiver接受广播事件 ====> 开启循环动作")

        //检测Activity栈里面是否有MainActivity
        if (ActivityManage.getActivityStack() == null || ActivityManage.getActivityStack().size == 0) {
            //重启首页
           // context.gotoActivity<DemoMainActivity>()
        } else {
            YYLogUtils.w("不需要重启,已经有栈在运行了 Size:" + ActivityManage.getActivityStack().size)
        }
    }

}

打印日志。

2.3 WorkManager实现定时任务

WorkManager的使用相对来说也比较简单, WorkManager组件库里面提供了一个专门做周期性任务的类PeriodicWorkRequest。但是PeriodicWorkRequest类有一个限制条件最小的周期时间是15分钟。

WorkManager 比较适合一些比较长时间的任务。还能设置一些约束条件,比如我们每24小时,在设备充电的时候我们就上传这一整天的Log文件到服务器,比如我们每隔12小时就检查应用是否需要更新,如果需要更新则自动下载安装(需要指定Root设备)。

场景如下,还是那个放在公司前台常亮并且一直运行在前台的平板,我们每12小时就检查自动更新,并自动安装,由于之前写了 AlarmManager 所以安装成功之后App会自动打开。

伪代码如下:

        Data inputData2 = new Data.Builder().putString("version", "1.0.0").build();
        PeriodicWorkRequest checkVersionRequest =
                new PeriodicWorkRequest.Builder(CheckVersionWork.class, 12, TimeUnit.HOURS)
                        .setInputData(inputData2).build();

        WorkManager.getInstance().enqueue(checkVersionRequest);
        WorkManager.getInstance().getWorkInfoByIdLiveData(checkVersionRequest.getId()).observe(this, workInfo -> {
            assert workInfo != null;
            WorkInfo.State state = workInfo.getState();

            Data data = workInfo.getOutputData();
            String url = data.getString("download_url", "");
             //去下载并静默安装Apk    
            downLoadingApkInstall(url)
        });
/**
 * 间隔12个小时的定时任务,检测版本的更新
 */
public class CheckVersionWork extends Worker {

    public CheckVersionWork(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @Override
    public void onStopped() {
        super.onStopped();
    }

    @NonNull
    @Override
    public Result doWork() {
        Data inputData = getInputData();
        String version = inputData.getString("version");

        //接口获取当前最新的信息

       //对比当前版本与服务器版本,是否需要更新

       //如果需要更新,返回更新下载的url

       Data outputData = new Data.Builder().putString("key_name", inputData.getString("download_url", "xxxxxx")).build();
      //设置输出数据
        setOutputData(outputData);

        return Result.success();
    }
}

那除此之外我们一些Log的上传,图片的更新,资源或插件的下载等,我们都可以通过WorkManager来实现一些后台的操作,使用起来也是很简单。

总结

这里我直接给出了一些特定的场景应该使用哪一种定时任务,如果大家的应用场景适合App内部的定时任务,应该优先选择内部的定时任务。

App外的定时任务,都是系统服务的定时任务,不一定保险,毕竟是和厂商(特别是国内的厂商)作对,厂商会想方设法杀死我们的定时任务,毕竟有风险。

来自:https://juejin.cn/post/7130793502933254180

上一篇下一篇

猜你喜欢

热点阅读