最近公司项目需求要在app内部维护一个时间.
背景:没有即时的网络,还得防止用户修改手机时间.
前提:记录时间的时候需要联网获取一次准确的时间
方案一:
利用android提供的api,即AlarmManager,这玩意可以每隔多少时间发送个广播。注册一个广播接受者,就可以获取到该广播。
/** * 开启时间同步广播 * * @param serverMills 该参数为第一次获取到的准确时间(时间戳) */ public void syncTimeByReceiver(long serverMills) { // 将该时间戳存入application的成员变量中.一会要在receiver中取值 App.getInstance().setCurrentMills(serverMills); // AlarmManager利用相对时间发送广播 Intent intent = new Intent(AlarmReceiver.ACTION_ALARM_SYNC_TIME); PendingIntent pi = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE); //因为我需要的是时间戳加上每次广播相隔的时间段.所以用相对时间.这样每隔TIME_SYNC_PERIOD就会收到一个广播 am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + AlarmReceiver.TIME_SYNC_PIROD, AlarmReceiver.TIME_SYNC_PERIOD, pi); }
这样每隔一个时间段就能收到广播了.
am.setRepeating(type, triggerAtMillis, intervalMillis, operation);
第一个参数表示闹钟类型,第二个参数表示闹钟首次执行时间,第三个参数表示闹钟两次执行的间隔时间,第四个参数表示闹钟响应动作.
type又分为
AlarmManager.ELAPSED_REALTIME表示闹钟在手机睡眠状态下不可用,该状态下闹钟使用相对时间(相对于系统启动开始),状态值为3;
AlarmManager.ELAPSED_REALTIME_WAKEUP表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟也使用相对时间,状态值为2;
AlarmManager.RTC表示闹钟在睡眠状态下不可用,该状态下闹钟使用绝对时间,即当前系统时间,状态值为1;
AlarmManager.RTC_WAKEUP表示闹钟在睡眠状态下会唤醒系统并执行提示功能,该状态下闹钟使用绝对时间,状态值为0;
AlarmManager.POWER_OFF_WAKEUP表示闹钟在手机关机状态下也能正常进行提示功能,所以是5个状态中用的最多的状态之一,该状态下闹钟也是用绝对时间,状态值为4;不过本状态好像受SDK版本影响,某些版本并不支持;
下边是receiver的代码
public class AlarmReceiver extends BroadcastReceiver { // 时间同步间隔(毫秒) public static int TIME_SYNC_PERIOD= 5 * 1000; /** * 广播接收的action */ public static String ACTION_ALARM_SYNC_TIME = "com.appxutils.receiver.AlarmReceiver"; @Override public void onReceive(Context context, Intent intent) { L.error("收到时间同步广播"); //将application中存入的时间戳取出加上TIME_SYNC_PERIOD long appCurSyncTime = App.getInstance().getCurrentMills() + TIME_SYNC_PERIOD; App.getInstance().setCurrentMills(appCurSyncTime); } }
问题:
在使用了几部手机测试后.发现手里有部红米手机接收不到广播.调查后发现小米的rom修改了AlarmManager的内部机制
为了省电,内部做了对齐唤醒机制. 只有用
am.setRepeating(AlarmManager.RTC, SystemClock.elapsedRealtime() + AlarmReceiver.TIME_SYNC_PIROD, AlarmReceiver.TIME_SYNC_PERIOD, pi);
但是用了RTC,是绝对时间不说时间长手机睡眠后广播又不正常了.
so采用广播的方案果断的被android的碎片化被干死了
方案二.service内维护一个计时器
原理:service里维护一个计时器,每隔一个时间段就把时间戳加上这个时间段
/** * 开始时间同步服务 * @param serverMills 该参数为第一次获取到的准确时间(时间戳) */ public void syncTimeByService(long serverMills) { //将该时间戳存入application的成员变量中.一会要在service中取值 App.getInstance().setCurrentMills(serverMills); startService(new Intent(this, TimeSync.class)); }
service代码
public class TimeSync extends Service { // 时间同步间隔(秒) public static int TIME_SYNC_PIROD = 5; @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { startTimeSync(); return Service.START_STICKY; } //开启定时器每隔5秒执行一次 private void startTimeSync() { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); executor.scheduleAtFixedRate(new Runnable() { @Override public void run() { App.getInstance().setCurrentMills(App.getInstance().getCurrentMills() + TIME_SYNC_PIROD); } }, 0, TIME_SYNC_PIROD , TimeUnit.SECONDS); } }
START_STICKY:如果service进程被kill掉,保留service的状态为开始状态,但不保留递送的intent对象。随后系统会尝试重新创建service,由于服务状态为开始状态,所以创建服务后一定会调用onStartCommand(Intent,int,int)方法。如果在此期间没有任何启动命令被传递到service,那么参数Intent将为null。
START_NOT_STICKY:“非粘性的”。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉,系统不会自动重启该服务
START_REDELIVER_INTENT:重传Intent。使用这个返回值时,如果在执行完onStartCommand后,服务被异常kill掉系统会自动重启该服务,并将Intent的值传入。
START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被kill后一定能重启。
注意这里不采用Timer,因为Timer使用的是绝对时间。
也就是说如果你设定的是每隔5分钟执行一次的话。此时把手机时间往后调了5分钟后。Timer就会立即再执行一次。
故ScheduledExecutorService用的是相对时间。
刚发现小米系统后台的service也会被挂起.,so第二种方案被android的碎片化给击毙了.
第三种方案.
是针对第二种方案的增强.由于我只是需要维护一个相对的时间戳,是为了防止用户在手机上修改时间.
故增加了监听屏幕关闭和点亮的广播事件.记录屏幕关闭和打开的时候的一个时间戳,用打开的时间减去关闭的时间,就得到了屏幕关闭的时间.然后让线程中维护的时间加上这个时间.
package com.appxutils.service; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import android.annotation.SuppressLint; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.util.Log; /** * 时间同步的service * * @description * @CreatedBy Rocky.Zhang * @date 2014年8月24日 */ public class TimeSyncService extends Service { // 时间同步间隔(毫秒) public static int TIME_SYNC_PIROD = 1 * 1000; /** * 当前时间戳 */ private static long CURRENT_MILLS = 0; /** * 当前屏幕状态 */ private static boolean IS_SCREEN_ON = true; private ScheduledExecutorService executor; @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { return Service.START_STICKY; } @Override public void onCreate() { startTimeSync(); super.onCreate(); } /** * 开启时间同步 */ private void startTimeSync() { executor = Executors.newScheduledThreadPool(1); executor.scheduleAtFixedRate(new Runnable() { @Override public void run() { if (IS_SCREEN_ON) {//只有屏幕点亮的时候,才去计时 CURRENT_MILLS = CURRENT_MILLS + TIME_SYNC_PIROD; } } }, 0, TIME_SYNC_PIROD, TimeUnit.MILLISECONDS); } @Override public void onDestroy() { super.onDestroy(); } /** * 设置当前时间 * * @param currentTime * (格式为 YYYY_MM_DD_HH_MM_SS) */ public static void setCurrentTime(String currentTime) { CURRENT_MILLS = format2TimeStamp(currentTime, YYYY_MM_DD_HH_MM_SS); } public static void setCurrentTime(String currentTime, String format) { CURRENT_MILLS = format2TimeStamp(currentTime, format); } /** * 获取当前时间以字符串返回 * * @return YYYY_MM_DD_HH_MM_SS_SSS */ public static String getCurrentTime() { return getCurrentTime(YYYY_MM_DD_HH_MM_SS_SSS); } /** * 获取当前日期 * * @return YYYY_MM_DD */ public static String getCurrentDate() { return getCurrentTime(YYYY_MM_DD); } /** * 以指定格式返回当前时间 * * @param format * @return */ public static String getCurrentTime(String format) { return format2String(CURRENT_MILLS, format); } /** * 获取当前时间戳 * * @return */ public static long getCurrentMills() { return CURRENT_MILLS; } /** * 设置当前时间 * * @param currentMills */ public static void setCurrentMills(long currentMills) { CURRENT_MILLS = currentMills; } public static final String YYYY_MM_DD_HH_MM_SS_SSS = "yyyy-MM-dd HH:mm:ss SSS"; public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; public static final String YYYY_MM_DD = "yyyy-MM-dd"; @SuppressLint("SimpleDateFormat") /** * 将时间戳格式化为字符串 * @param milliseconds * @param format * @return */ public static String format2String(Long milliseconds, String format) { String result = ""; try { Date date = new Date(milliseconds); if (date != null) { DateFormat dateFormat = new SimpleDateFormat(format); result = dateFormat.format(date); } } catch (Exception e) { throw new RuntimeException("时间格式与fomat类型不匹配"); } return result; } @SuppressLint("SimpleDateFormat") private static long format2TimeStamp(String currentTime, String format) { long timeStamp = 0; try { Date date = null; SimpleDateFormat dateFormat = new SimpleDateFormat(format); date = dateFormat.parse(currentTime); timeStamp = date.getTime(); } catch (Exception e) { throw new RuntimeException("时间格式与fomat类型不匹配"); } return timeStamp; } /** * 屏幕关闭的时间 */ public static long SCREEN_OFF_MILLS = 0; /** * 屏幕打开的时间 */ public static long SCREEN_ON_MILLS = 0; public static class ScreenStatusReceiver extends BroadcastReceiver { public static final String ACTION_SCREEN_ON = "android.intent.action.SCREEN_ON"; public static final String ACTION_SCREEN_OFF = "android.intent.action.SCREEN_OFF"; @Override public void onReceive(Context context, Intent intent) { if (ACTION_SCREEN_ON.equals(intent.getAction())) { IS_SCREEN_ON = true; SCREEN_ON_MILLS = System.currentTimeMillis(); //接收到屏幕被打开的广播,将CURRENT_MILLS加上屏幕关闭的时间 CURRENT_MILLS = CURRENT_MILLS + (SCREEN_ON_MILLS - SCREEN_OFF_MILLS); } else if (ACTION_SCREEN_OFF.equals(intent.getAction())) { IS_SCREEN_ON = false; SCREEN_OFF_MILLS = System.currentTimeMillis(); } } } }
记得在application中注册和注销广播
IntentFilter filter = new IntentFilter(); filter.addAction(TimeSyncService.ScreenStatusReceiver.ACTION_SCREEN_OFF); filter.addAction(TimeSyncService.ScreenStatusReceiver.ACTION_SCREEN_ON); screenStatusReceiver = new TimeSyncService.ScreenStatusReceiver(); registerReceiver(screenStatusReceiver, filter);
unregisterReceiver(screenStatusReceiver);
目前这种方案在小米手机上测试通过.
大家有更好的方案.请留言给我.