笔记58 | Android闹钟

调度重复的闹钟

闹钟(基于AlarmManager类)给予你一种在应用使用期之外执行与时间相关的操作的方法。你可以使用闹钟初始化一个长时间的操作,例如每天开启一次后台服务,下载当日的天气预报。

闹钟具有如下特性:

  • 允许你通过预设时间或者设定某个时间间隔,来触发Intent;
  • 你可以将它与BroadcastReceiver相结合,来启动服务并执行其他操作;
  • 可在应用范围之外执行,所以你可以在你的应用没有运行或设备处于睡眠状态的情况下,使用它来触发事件或行为;
  • 帮助你的应用最小化资源需求,你可以使用闹钟调度你的任务,来替代计时器或者长时间连续运行的后台服务。

Note:对于那些需要确保在应用使用期之内发生的定时操作,可以使用闹钟替代使用Handler结合Timer与Thread的方法。因为它可以让Android系统更好地统筹系统资源。

权衡利弊

重复闹钟的机制比较简单,没有太多的灵活性。它对于你的应用来说或许不是一种最好的选择,特别是当你想要触发网络操作的时候。设计不佳的闹钟会导致电量快速耗尽,而且会对服务端产生巨大的负荷。

当我们从服务端同步数据时,往往会在应用不被使用的时候时被唤醒触发执行某些操作。此时你可能希望使用重复闹钟。但是如果存储数据的服务端是由你控制的,使用Google Cloud Messaging(GCM)结合sync adapter是一种更好解决方案。SyncAdapter提供的任务调度选项和AlarmManager基本相同,但是它能提供更多的灵活性。比如:同步的触发可能基于一条“新数据”提示消息,而消息的产生可以基于服务器或设备,用户的操作(或者没有操作),每天的某一时刻等等。

最佳实践方法

在设计重复闹钟过程中,你所做出的每一个决定都有可能影响到你的应用将会如何使用系统资源。例如,我们假想一个会从服务器同步数据的应用。同步操作基于的是时钟时间,具体来说,每一个应用的实例会在下午十一点整进行同步,巨大的服务器负荷会导致服务器响应时间变长,甚至拒绝服务。因此在我们使用闹钟时,请牢记下面的最佳实践建议:

  • 对任何由重复闹钟触发的网络请求添加一定的随机性(抖动):
    • 在闹钟触发时做一些本地任务。“本地任务”指的是任何不需要访问服务器或者从服务器获取数据的任务;
    • 同时对于那些包含有网络请求的闹钟,在调度时机上增加一些随机性。
  • 尽量让你的闹钟频率最小;
  • 如果不是必要的情况,不要唤醒设备(这一点与闹钟的类型有关,本节课后续部分会提到);
  • 触发闹钟的时间不必过度精确; 尽量使用 setInexactRepeating()方法替代 setRepeating()方法。当你使用 setInexactRepeating()方法时,Android系统会集中多个应用的重复闹钟同步请求,并一起触发它们。这可以减少系统将设备唤醒的总次数,以此减少电量消耗。从Android 4.4(API Level19)开始,所有的重复闹钟都将是非精确型的。注意虽然 setInexactRepeating()setRepeating()的改进版本,它依然可能会导致每一个应用的实例在某一时间段内同时访问服务器,造成服务器负荷过重。因此如之前所述,对于网络请求,我们需要为闹钟的触发时机增加随机性。
  • 尽量避免让闹钟基于时钟时间。

想要在某一个精确时刻触发重复闹钟是比较困难的。我们应该尽可能使用ELAPSED_REALTIME。不同的闹钟类型会在本节课后半部分展开。

设置重复闹钟

如上所述,对于定期执行的任务或者数据查询而言,使用重复闹钟是一个不错的选择。它具有下列属性:

  • 闹钟类型(后续章节中会展开讨论);
  • 触发时间。如果触发时间是过去的某个时间点,闹钟会立即被触发;
  • 闹钟间隔时间。例如,一天一次,每小时一次,每五秒一次,等等;
  • 在闹钟被触发时才被发出的Pending Intent。如果你为同一个Pending Intent设置了另一个闹钟,那么它会将第一个闹钟覆盖。

选择闹钟类型

使用重复闹钟要考虑的第一件事情是闹钟的类型。

闹钟类型有两大类: ELAPSED_REALTIMEREAL_TIME_CLOCK(RTC)。 ELAPSED_REALTIME从系统启动之后开始计算, REAL_TIME_CLOCK使用的是世界统一时间(UTC)。也就是说由于 ELAPSED_REALTIME不受地区和时区的影响,所以它适合于基于时间差的闹钟(例如一个每过30秒触发一次的闹钟)。 REAL_TIME_CLOCK适合于那些依赖于地区位置的闹钟。

两种类型的闹钟都还有一个唤醒( WAKEUP)版本,也就是可以在设备屏幕关闭的时候唤醒CPU。这可以确保闹钟会在既定的时间被激活,这对于那些实时性要求比较高的应用(比如含有一些对执行时间有要求的操作)来说非常有效。如果你没有使用唤醒版本的闹钟,那么所有的重复闹钟会在下一次设备被唤醒时被激活。

如果你只是简单的希望闹钟在一个特定的时间间隔被激活(例如每半小时一次),那么你可以使用任意一种 ELAPSED_REALTIME类型的闹钟,通常这会是一个更好的选择。

如果你的闹钟是在每一天的特定时间被激活,那么你可以选择 REAL_TIME_CLOCK类型的闹钟。不过需要注意的是,这个方法会有一些缺陷——如果地区发生了变化,应用可能无法做出正确的改变;另外,如果用户改变了设备的时间设置,这可能会造成应用产生预期之外的行为。使用 REAL_TIME_CLOCK类型的闹钟还会有精度的问题,因此我们建议你尽可能使用 ELAPSED_REALTIME类型。

下面列出闹钟的具体类型:

  • ELAPSED_REALTIME:从设备启动之后开始算起,度过了某一段特定时间后,激活Pending Intent,但不会唤醒设备。其中设备睡眠的时间也会包含在内。
  • ELAPSEDREALTIMEWAKEUP:从设备启动之后开始算起,度过了某一段特定时间后唤醒设备。
  • RTC:在某一个特定时刻激活Pending Intent,但不会唤醒设备。
  • RTC_WAKEUP:在某一个特定时刻唤醒设备并激活Pending Intent。

ELAPSEDREALTIMEWAKEUP案例

下面是使用ELAPSEDREALTIMEWAKEUP的例子。

每隔在30分钟后唤醒设备以激活闹钟:

// Hopefully your alarm will have a lower frequency than this!
alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        AlarmManager.INTERVAL_HALF_HOUR,
        AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);

在一分钟后唤醒设备并激活一个一次性(无重复)闹钟:

private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        SystemClock.elapsedRealtime() +
        60 * 1000, alarmIntent);

RTC案例

下面是使用RTC_WAKEUP的例子。

在大约下午2点唤醒设备并激活闹钟,并不断重复:

// Set the alarm to start at approximately 2:00 p.m.
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 14);

// With setInexactRepeating(), you have to use one of the AlarmManager interval
// constants--in this case, AlarmManager.INTERVAL_DAY.
alarmMgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
        AlarmManager.INTERVAL_DAY, alarmIntent);

让设备精确地在上午8点半被唤醒并激活闹钟,自此之后每20分钟唤醒一次:

private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

// Set the alarm to start at 8:30 a.m.
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 30);

// setRepeating() lets you specify a precise custom interval--in this case,
// 20 minutes.
alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
        1000 * 60 * 20, alarmIntent);

决定闹钟的精确度

如上所述,创建闹钟的第一步是要选择闹钟的类型,然后你需要决定闹钟的精确度。对于大多数应用而言, setInexactRepeating()会是一个正确的选择。当你使用该方法时,Android系统会集中多个应用的重复闹钟同步请求,并一起触发它们。这样可以减少电量的损耗。

对于另一些实时性要求较高的应用——例如,闹钟需要精确地在上午8点半被激活,并且自此之后每隔1小时激活一次——那么可以使用 setRepeating()。不过你应该尽量避免使用精确的闹钟。

使用 setRepeating()时,你可以制定一个自定义的时间间隔,但在使用 setInexactRepeating()时不支持这么做。此时你只能选择一些时间间隔常量,例如:INTERVALFIFTEENMINUTES

,INTERVAL_DAY等。完整的常量列表,可以查看AlarmManager。

取消闹钟

你可能希望在应用中添加取消闹钟的功能。要取消闹钟,可以调用AlarmManager的 cancel()方法,并把你不想激活的PendingIntent传递进去,例如:

// If the alarm has been set, cancel it.
if (alarmMgr!= null) {
    alarmMgr.cancel(alarmIntent);
}

在设备启动后启用闹钟

默认情况下,所有的闹钟会在设备关闭时被取消。要防止闹钟被取消,你可以让你的应用在用户重启设备后自动重启一个重复闹钟。这样可以让AlarmManager继续执行它的工作,且不需要用户手动重启闹钟。

具体步骤如下:

1.在应用的Manifest文件中设置RECEIVEBOOTCMPLETED权限,这将允许你的应用接收系统启动完成后发出的ACTIONBOOTCOMPLETED广播(只有在用户至少将你的应用启动了一次后,这样做才有效):

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

2.实现BoradcastReceiver用于接收广播:

public class SampleBootReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
            // Set the alarm here.
        }
    }
}

3.在你的Manifest文件中添加一个接收器,其Intent-Filter接收ACTIONBOOTCOMPLETED这一Action:

<receiver android:name=".SampleBootReceiver"
        android:enabled="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"></action>
    </intent-filter>
</receiver>

注意Manifest文件中,对接收器设置了 android:enabled="false"属性。这意味着除非应用显式地启用它,不然该接收器将不被调用。这可以防止接收器被不必要地调用。你可以像下面这样启动接收器(比如用户设置了一个闹钟):

ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
        PackageManager.DONT_KILL_APP);

一旦你像上面那样启动了接收器,它将一直保持启动状态,即使用户重启了设备也不例外。换句话说,通过代码设置的启用配置将会覆盖掉Manifest文件中的现有配置,即使重启也不例外。接收器将保持启动状态,直到你的应用将其禁用。你可以像下面这样禁用接收器(比如用户取消了一个闹钟):

ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
        PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
        PackageManager.DONT_KILL_APP);

所有代码: Main:

package com.example.alarmtest;

import java.util.Calendar;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.app.TimePickerDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TimePicker;
import android.widget.Toast;

public class MainActivity extends Activity {
    Button button;
    AlarmManager alarmManager;
    int hour,min;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.button1);
        //获取AlarmManager对象
        alarmManager = (AlarmManager) getSystemService(Service.ALARM_SERVICE);

        button.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                getData();
//                Calendar currentTime = Calendar.getInstance();
                //创建一个TimePickerDialog实例,并显示
                new TimePickerDialog(MainActivity.this, 0, new TimePickerDialog.OnTimeSetListener() {

                            @Override
                            public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
                                //指定启动AlarmActivity组件
                                Intent intent = new Intent(MainActivity.this, AlarmActivity.class);
                                //创建PendingIntent对象
                                /*
                                 * PendingIntent与Intent的区别是PendingIntent处理即将发生的事情
                                 * 比如:在通知栏Notification中跳转页面,不是立即跳转
                                 * 通常通过  getActivity、getBroadcast、getService得到PendingIntent的实例
                                 */
                                PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0, intent, 0);
                                Calendar c = Calendar.getInstance();
                                c.setTimeInMillis(System.currentTimeMillis());

                                Log.i("md", "Calendar.HOUR:"+hourOfDay+"  Calendar.MINUTE:"+minute);
                                c.set(Calendar.HOUR, hourOfDay);
                                c.set(Calendar.MINUTE, minute);

                                hour = hourOfDay;
                                min = minute;
                                write(hour,min);
                                try {
                                    alarmManager.set(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), pi);
                                    Toast.makeText(MainActivity.this, "设置闹钟成功", Toast.LENGTH_SHORT).show();
                                } catch (Exception e) {
                                    Toast.makeText(MainActivity.this, "设置闹钟失败", Toast.LENGTH_SHORT).show();
                                    Log.i("md", "e:"+e.toString());
                                }
                            }
                }, hour, min, false).show();
            }
        });
    }

    private void getData() {
           SharedPreferences preferences_eo = this.getSharedPreferences("alarm_time", Context.MODE_PRIVATE); 
           hour = preferences_eo.getInt("hourOfDay", 0);
           min = preferences_eo.getInt("minute", 0);
    }

    private void write(int hourOfDay,int minute){  
        SharedPreferences preferences = this.getSharedPreferences("alarm_time", Context.MODE_PRIVATE);  
        Editor editor = preferences.edit();  
        editor.putInt("hourOfDay", hourOfDay);  
        editor.putInt("minute", minute);  
        editor.commit();  
    } 
}

AlarmActivity:

import java.util.Calendar;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;

public class AlarmActivity extends Activity{
    MediaPlayer alarmMusic;
    private AlertDialog alert;  
    private AlertDialog.Builder builder=null; 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        alarmMusic_play();
        alert=null;  
        builder=new AlertDialog.Builder(this);  
        alert=builder.setIcon(R.drawable.icon_logo)//图标  
                .setTitle("Clock")  
                .setMessage("Get up quickly, the LaoWang is already exercising his waist!")  
                .setNegativeButton("延迟1分钟", new OnClickListener() {  

                    @Override  
                    public void onClick(DialogInterface dialog, int which) { 
                        alarmMusic_stop();
                        alarmdelay();
                        AlarmActivity.this.finish();
                    }  
                }).setPositiveButton("确认", new OnClickListener() {  

                    @Override  
                    public void onClick(DialogInterface dialog, int which) {  
                        alarmMusic_stop();
                        AlarmActivity.this.finish();
                    }  
                }).create();//创建  
        alert.show();//显示  
    }

    private void alarmMusic_play(){
        //加载指定音乐,并为之创建MediaPlayer对象
        alarmMusic = MediaPlayer.create(this, R.raw.jin);
        alarmMusic.setLooping(true);
        //播放闹钟
        alarmMusic.start();
    }

    private void alarmMusic_stop(){
        //停止音乐
        alarmMusic.stop();
        // If the alarm has been set, cancel it.
        if (alarmMgr!= null) {
            alarmMgr.cancel(alarmIntent);
        }
    }

    private AlarmManager alarmMgr;
    private PendingIntent alarmIntent;
    private void alarmdelay() {
        alarmMgr = (AlarmManager)this.getSystemService(Context.ALARM_SERVICE);
        Intent intent = new Intent(this, MainActivity.class);
        alarmIntent = PendingIntent.getBroadcast(this, 0, intent, 0);

        // Set the alarm to start at 8:30 a.m.
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(System.currentTimeMillis());
        int hour = calendar.get(Calendar.HOUR);
        int min = calendar.get(Calendar.MINUTE);
        calendar.set(Calendar.HOUR, hour);
        calendar.set(Calendar.MINUTE, min);

        Log.i("md", "hour: "+hour+"  min: "+min);
        // setRepeating() lets you specify a precise custom interval--in this case,
        // 20 minutes.
        alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
                1000 * 60 * 1, alarmIntent);
    }
}

广播:

package com.example.alarmtest;

import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;

public class SampleBootReceiver extends BroadcastReceiver{

    @Override
    public void onReceive(Context context, Intent intent) {
           if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
               ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);

               PackageManager pm = context.getPackageManager();
               pm.setComponentEnabledSetting(receiver,
                       PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                       PackageManager.DONT_KILL_APP);
               /*
                * 禁用闹钟
                */
//               ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
//               PackageManager pm = context.getPackageManager();
//
//               pm.setComponentEnabledSetting(receiver,
//                       PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
//                       PackageManager.DONT_KILL_APP);
            }
    }

}

原文发布于微信公众号 - 项勇(xiangy_life)

原文发表时间:2017-12-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏飞雪无情的博客

Go语言经典库使用分析(三)| Gorilla Handlers 详细介绍

在我们编写web服务端程序的时候,我们可能会对一些甚至全部的Http Request统一处理,比如我们记录每个访问的Request,对提交的Form表单进行映射...

781
来自专栏安富莱嵌入式技术分享

【二代示波器教程】第13章 RTX操作系统版本二代示波器实现

本章教程为大家讲解RTX操作系统版本的二代示波器实现。主要讲解RTOS设计框架,即各个任务实现的功能,任务间的通信方案选择,任务栈,系统栈以及全局变量共享问题。...

822
来自专栏HBStream流媒体与音视频技术

设计并实现同时支持多种视频格式的流媒体点播系统

2705
来自专栏何俊林

插件开发之360 DroidPlugin源码分析(四)Activity预注册占坑

在了解系统的activity,service,broadcastReceiver的启动过程后,今天将分析下360 DroidPlugin是如何预注册占坑的?本篇...

1807
来自专栏向治洪

Rxjava + retrofit + dagger2 + mvp搭建Android框架

最近出去面试,总会被问到我们项目现在采用的什么开发框架,不过据我的经验网络框架(volley)+图片缓存(uIl)+数据库(orm)+mvp,不过现在这套框架比...

19410
来自专栏蜉蝣禅修之道

Android开发之声网即时通讯与讯飞语音识别相结合

2293
来自专栏非著名程序员

良心推荐:总结 Android 开发中必备的代码 Review 清单

本文收集了我自己工作以来提交代码前的所有检查点。事实证明,这样能有效提高自己的代码质量和功能的稳定性。所以推荐大家以后每次提交代码前,都可以看下这份 Revie...

1955
来自专栏码洞

轻量级框架Spark快速入门

尝试过Python/Ruby/Nodejs/Golang语言开发的人往往难以适应Java Web框架,相对于这些语言提供的web框架来说,Java的Web框架显...

752
来自专栏有趣的Python

Scrapy分布式爬虫打造搜索引擎-(五)爬虫与反爬虫的战争Python分布式爬虫打造搜索引擎

Python分布式爬虫打造搜索引擎 基于Scrapy、Redis、elasticsearch和django打造一个完整的搜索引擎网站 五、爬虫与反爬虫 1. 基...

3414
来自专栏向治洪

android性能优化1

一、在使用Gallery控件时,如果载入的图片过多,过大,就很容易出现OutOfMemoryError异常,就是内存溢出。这是因为Android默认分配的内存只...

1936

扫码关注云+社区