android app内部维护一个时间

最近公司项目需求要在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);


目前这种方案在小米手机上测试通过.


大家有更好的方案.请留言给我.

你可能感兴趣的:(android app内部维护一个时间)