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

相比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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容