Android 后台定时任务

项目中需要做一个定时本地通知,本文是自己code时所遇到的问题以及解决方案的总结,其中借鉴了一些文章中的解决方式,文章后有借鉴文章链接和项目github地址。
Android 中的定时任务一般有两种实现方式,一种是使用 Java API 里提供的 Timer 类,一种是使用 Android 的 Alarm 机制。这两种方式在多数情况下都能实现类似的效果。
  • Timer并不太适用于那些需要长期在后台运行的定时任务。为了能让电池更加耐用,每种手机都会有自己的休眠策略,Android 手机就会在长时间不操作的情况下自动让 CPU 进入到睡眠状态,这就有可能导致 Timer 中的定时任务无法正常运行。
  • Alarm具有唤醒 CPU 的功能,即可以保证每次需要执行定时任务的时候 CPU 都能正常工作。

Alarm主要是借助AlarmManager类来实现,Android 官方文档对AlarmManager解释如下

该类提供对系统报警服务的访问。这些允许您安排您的应用程序在将来的某个时间运行。当报警熄灭时,已注册的意图由系统进行广播,如果尚未运行,则自动启动目标应用程序...
...
注意:从API 19(KITKAT)开始,报警传递不准确:操作系统将移动报警,以最大限度地减少唤醒和电池使用。有新的API支持需要严格交付保证的应用程序
...

设置定时任务,API大于19会有报警时间不准确,API大于23时Doze模式系统将尝试减少设备的唤醒频率推迟后台作业可能导致无法执行,我们需要根据版本分别适配。同时用BroadcastReceiver接受提醒并执行任务

    Intent alarmIntent = new Intent();
    alarmIntent.setAction(TamingReceiver.ALARM_WAKE_ACTION);
    PendingIntent operation = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    alarmManager.cancel(operation);
    long triggerAtMillis = task.triggerAtMillis();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), operation);
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), operation);
    } else {
        alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), operation);
    }
    //接受任务提醒
    public class TamingReceiver extends BroadcastReceiver {
    
        public static final String ALARM_WAKE_ACTION = "youga.tamingtask.taming.ALARM_WAKE_ACTION";
    
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d(TAG, "onReceive()--Action:" + intent.getAction());
        }
    }

实际使用时AlarmManager无论在应用是否被关闭都能正常执行,但这仅限于原生Android系统。国产定制Android系统小米、魅族、华为等等都会对AlarmManager唤醒做限制,导致应用被关闭后无法正常执行。此时我们需要做的就是应用保活

应用保活可以分为两个方面,一. 提供进程优先级,降低进程被杀死的概率,二. 在进程被杀死后,进行拉活

提升进程优先级的方案可分为Activity 提升权限, Notification 提升权限

  • Activity 提升权限有网传QQ一像素Activity方案,该方案涉及触摸时间拦截,各种状态监听操作难度复杂。

  • Notification 提升权限,API小于18可以直接设置前台Notification。API大于18利用系统漏洞,两个Service共同设置同一个ID 的前台Notification,并关闭其中一个Service,Notification消失,另一个Service优先级不变,此漏洞API=24时被修复

     public void onCreate() {
          super.onCreate();
          Log.d(TAG, "onCreate()");
    
          Daemon.run(TamingService.this, TamingService.class, Daemon.INTERVAL_ONE_MINUTE);
    
          Intent service = new Intent(this, TamingGuardService.class);
          startService(service);
    
          if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
              startForeground(GRAY_SERVICE_ID, new Notification());
          } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
              Intent innerIntent = new Intent(this, TamingInnerService.class);
              startService(innerIntent);
              startForeground(GRAY_SERVICE_ID, new Notification());
          } else {
              // TODO: 2017/9/22 0022
    
          }
     }
    

进程死后拉活的方案可分为系统广播拉活,利用系统Service机制拉活,利用Native进程拉活

  • 广播接收器被管理软件、系统软件通过“自启管理”等功能禁用的场景无法接收到广播,从而无法自启,系统广播事件不可控,只能保证发生事件时拉活进程,但无法保证进程挂掉后立即拉活。

      <receiver
          android:name=".taming.WakeUpReceiver"
          android:process=":Taming">
          <intent-filter>
              <action android:name="android.intent.action.USER_PRESENT"/>
              <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
              <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
          </intent-filter>
          <intent-filter>
              <action android:name="android.intent.action.BOOT_COMPLETED"/>
              <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
          </intent-filter>
          <intent-filter>
              <action android:name="android.intent.action.PACKAGE_ADDED"/>
              <action android:name="android.intent.action.PACKAGE_REMOVED"/>
    
              <data android:scheme="package"/>
          </intent-filter>
      </receiver>
    
  • 同时我们启动另个守护GuardService监听这个Service状态,如果发现这个被异常Service关闭则启动这个Service,API小于21我们用AlarmManager重复监听,API大于21我们使用JobScheduler监听,然而JobScheduler在API大于24时Doze模式会因为电池优化而无法正常执行,我们需要忽略电池优化

    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }
    //守护GuardService
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
        JobInfo.Builder builder = new JobInfo.Builder(0, new ComponentName(getPackageName(), JobSchedulerService.class.getName()));
        builder.setPeriodic(JOB_INTERVAL); //每隔60秒运行一次
        //Android 7.0+ 增加了一项针对 JobScheduler 的新限制,最小间隔只能是下面设定的数字
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            builder.setPeriodic(JobInfo.getMinPeriodMillis(), JobInfo.getMinFlexMillis());
        }
        builder.setRequiresCharging(true);
        builder.setPersisted(true);  //设置设备重启后,是否重新执行任务
        builder.setRequiresDeviceIdle(true);

        if (jobScheduler.schedule(builder.build()) <= 0) {
            Log.w("init", "jobScheduler.schedule something goes wrong");
        }
    } else {
        //发送唤醒广播来促使挂掉的UI进程重新启动起来
        AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        Intent alarmIntent = new Intent(this, TamingService.class);
        alarmIntent.setAction(TamingService.GUARD_INTERVAL_ACTION);
        PendingIntent operation = PendingIntent.getService(this, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), ALARM_INTERVAL, operation);
    }
    
   //忽略电池优化
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
       PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
       boolean ignoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(context.getPackageName());
       if (!ignoringBatteryOptimizations) {
           Intent dozeIntent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
           dozeIntent.setData(Uri.parse("package:" + context.getPackageName()));
           startActivity(dozeIntent);
       }
   }
  • 利用Native进程拉活,我们采用开源的守护进程库。Android-AppDaemon。该方案主要适用于 Android5.0 以下版本手机。该方案不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以下版本拉活效果非常好。对于 Android5.0 以上手机,系统虽然会将native进程内的所有进程都杀死,这里其实就是系统“依次”杀死进程时间与拉活逻辑执行时间赛跑的问题,如果可以跑的比系统逻辑快,依然可以有效拉起。

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate()");
    
        Daemon.run(TamingService.this, TamingService.class, Daemon.INTERVAL_ONE_MINUTE);
    }
    

综上所述,Android原生系统使用AlarmManager执行定时任务无需处于运行中,但是手机重启后会清除所有Alarm,所以最终导向还是应用保活。

非原生Android系统会对系统修改我们需要各种方式配合,保证最大几率保活。一Notification 提升权限,二监听解锁监控电池电量和充电状态,开机广播,网络变化,应用安装、卸载,三守护GuardService监听,四利用Native进程拉活。然而这四中方式都无法保证百分百保活,我们还需要根据各机型适配,引导用户加入手机白名单,每个手机厂商各个版本白名单方式都会变化,适配路漫漫其修远兮,只能祈祷国内Android生态越来越好吧。

        //华为 自启管理
        Intent huaweiIntent = new Intent();
        huaweiIntent.setAction("huawei.intent.action.HSM_BOOTAPP_MANAGER");

        //华为 锁屏清理
        Intent huaweiGodIntent = new Intent();
        huaweiGodIntent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity"));

        //小米 自启动管理
        Intent xiaomiIntent = new Intent();
        xiaomiIntent.setAction("miui.intent.action.OP_AUTO_START");
        xiaomiIntent.addCategory(Intent.CATEGORY_DEFAULT);

        //小米 神隐模式
        Intent xiaomiGodIntent = new Intent();
        xiaomiGodIntent.setComponent(new ComponentName("com.miui.powerkeeper", "com.miui.powerkeeper.ui.HiddenAppsConfigActivity"));
        xiaomiGodIntent.putExtra("package_name", context.getPackageName());
        xiaomiGodIntent.putExtra("package_label", getApplicationName(context));

        //三星 5.0/5.1 自启动应用程序管理
        Intent samsungLIntent = context.getPackageManager().getLaunchIntentForPackage("com.samsung.android.sm");

        //三星 6.0+ 未监视的应用程序管理
        Intent samsungMIntent = new Intent();
        samsungMIntent.setComponent(new ComponentName("com.samsung.android.sm_cn", "com.samsung.android.sm.ui.battery.BatteryActivity"));

        //魅族 自启动管理
        Intent meizuIntent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
        meizuIntent.addCategory(Intent.CATEGORY_DEFAULT);
        meizuIntent.putExtra("packageName", context.getPackageName());

        //魅族 待机耗电管理
        Intent meizuGodIntent = new Intent();
        meizuGodIntent.setComponent(new ComponentName("com.meizu.safe", "com.meizu.safe.powerui.PowerAppPermissionActivity"));

        //Oppo 自启动管理
        Intent oppoIntent = new Intent();
        oppoIntent.setComponent(new ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity"));

        //Oppo 自启动管理(旧版本系统)
        Intent oppoOldIntent = new Intent();
        oppoOldIntent.setComponent(new ComponentName("com.color.safecenter", "com.color.safecenter.permission.startup.StartupAppListActivity"));

        //Vivo 后台高耗电
        Intent vivoGodIntent = new Intent();
        vivoGodIntent.setComponent(new ComponentName("com.vivo.abe", "com.vivo.applicationbehaviorengine.ui.ExcessivePowerManagerActivity"));

        //金立 应用自启
        Intent gioneeIntent = new Intent();
        gioneeIntent.setComponent(new ComponentName("com.gionee.softmanager", "com.gionee.softmanager.MainActivity"));

        //乐视 自启动管理
        Intent letvIntent = new Intent();
        letvIntent.setComponent(new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.AutobootManageActivity"));

        //乐视 应用保护
        Intent letvGodIntent = new Intent();
        letvGodIntent.setComponent(new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.BackgroundAppManageActivity"));

        //酷派 自启动管理
        Intent coolpadIntent = new Intent();
        coolpadIntent.setComponent(new ComponentName("com.yulong.android.security", "com.yulong.android.seccenter.tabbarmain"));

        //联想 后台管理
        Intent lenovoIntent = new Intent();
        lenovoIntent.setComponent(new ComponentName("com.lenovo.security", "com.lenovo.security.purebackground.PureBackgroundActivity"));

        //联想 后台耗电优化
        Intent lenovoGodIntent = new Intent();
        lenovoGodIntent.setComponent(new ComponentName("com.lenovo.powersetting", "com.lenovo.powersetting.ui.Settings$HighPowerApplicationsActivity"));

        //中兴 自启管理
        Intent zteIntent = new Intent();
        zteIntent.setComponent(new ComponentName("com.zte.heartyservice", "com.zte.heartyservice.autorun.AppAutoRunManager"));

        //中兴 锁屏加速受保护应用
        Intent zteGodIntent = new Intent();
        zteGodIntent.setComponent(new ComponentName("com.zte.heartyservice", "com.zte.heartyservice.setting.ClearAppSettingsActivity"));

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,251评论 25 707
  • 如何能让我们的应用能够在系统后台持续地运行是一个自Android从娘胎里出来时就议论不停的话题,而且这似乎成了一个...
    骏骏的简书阅读 1,101评论 1 19
  • 如何进行进程保活,首先我们应该先分析一下进程被杀死的原因开始 Android进程被杀死的场景分析: 从 Andro...
    编程小猪阅读 3,475评论 1 14
  • 昨天半夜睡不着,在朋友圈里发消息,不到一分钟,闺蜜子龙少奶奶出现了。不断地开导我,还安慰我让我暖暖的,安心睡觉。她...
    马奇朵阅读 218评论 0 0
  • 说了多少年的把我命运,怎么个把法。如果一个人的连自己的体态都管不好,还想把握机会,那是很小的概率。 管好饮食 管好...
    阳光滕君阅读 207评论 0 0