背景:前段时间要做一个用户行为统计分析的SDK,其中要做到一个功能是,比如用户更改了手机的时间以及年份日期等,那么此时获取的System.currentTimeMillis()就不准确,本人也是通过研究全球各大第三方顶尖统计开源项目+结合优化得出以下方案,无论用户如何更改手机本地的时间或者日期都可以获取当前用户产生的精确时间(已验证)
使用此功能需要了解以下api:
//什么是开机时间?返回自启动以来的毫秒数,包括睡眠时间
long elapsedRealtime = SystemClock.elapsedRealtime();
//什么是ntp网络时间? 是网络时间协议(Network Time Protocol),它是用来同步网络中各个计算机的时间的协议。直接copy复制以下NTPClient代码即可。
解决方案:NTP网络协议时间+开机时间(返回自启动以来的毫秒数,包括睡眠时间)
步骤一:新建一个类可以命名为NTPClient.java或其他自定义名称(已验证,可直接copy使用)
public class NTPClient {
private static final int ORIGINATE_TIME_OFFSET = 24;
private static final int RECEIVE_TIME_OFFSET = 32;
private static final int TRANSMIT_TIME_OFFSET = 40;
private static final int NTP_PACKET_SIZE = 48;
private static final int NTP_PORT = 123;
private static final int NTP_MODE_CLIENT = 3;
private static final int NTP_VERSION = 3;
// Number of seconds between Jan 1, 1900 and Jan 1, 1970
// 70 years plus 17 leap days
private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
private long mOffSet;
public long getOffset() {
return mOffSet;
}
public boolean requestTime(String host, int timeout) {
DatagramSocket socket = null;
try {
socket = new DatagramSocket();
socket.setSoTimeout(timeout);
InetAddress address = InetAddress.getByName(host);
byte[] buffer = new byte[NTP_PACKET_SIZE];
DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT);
// set mode = 3 (client) and version = 3
// mode is in low 3 bits of first byte
// version is in bits 3-5 of first byte
buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3);
// get current time and write it to the request packet
long requestTime = System.currentTimeMillis();
long requestTicks = SystemClock.elapsedRealtime();
writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime);
socket.send(request);
// read the response
DatagramPacket response = new DatagramPacket(buffer, buffer.length);
socket.receive(response);
long responseTicks = SystemClock.elapsedRealtime();
long responseTime = requestTime + (responseTicks - requestTicks);
// extract the results
long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime);
// receiveTime = originateTime + transit + skew
// responseTime = transmitTime + transit - skew
// clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2
// = ((originateTime + transit + skew - originateTime) +
// (transmitTime - (transmitTime + transit - skew)))/2
// = ((transit + skew) + (transmitTime - transmitTime - transit + skew))/2
// = (transit + skew - transit + skew)/2
// = (2 * skew)/2 = skew
long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2;
// if (false) Log.d(TAG, "round trip: " + roundTripTime + " ms");
// if (false) Log.d(TAG, "clock offset: " + clockOffset + " ms");
mOffSet = clockOffset;
} catch (Exception e) {
return false;
} finally {
if (socket != null) {
socket.close();
}
}
return true;
}
/**
* Reads an unsigned 32 bit big endian number from the given offset in the buffer.
*/
private long read32(byte[] buffer, int offset) {
byte b0 = buffer[offset];
byte b1 = buffer[offset + 1];
byte b2 = buffer[offset + 2];
byte b3 = buffer[offset + 3];
// convert signed bytes to unsigned values
int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0);
int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1);
int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2);
int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3);
return ((long) i0 << 24) + ((long) i1 << 16) + ((long) i2 << 8) + (long) i3;
}
/**
* Reads the NTP time stamp at the given offset in the buffer and returns * it as a system time (milliseconds since January 1, 1970).
*/
private long readTimeStamp(byte[] buffer, int offset) {
long seconds = read32(buffer, offset);
long fraction = read32(buffer, offset + 4);
return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L);
}
/**
* Writes system time (milliseconds since January 1, 1970) as an NTP time stamp * at the given offset in the buffer.
*/
private void writeTimeStamp(byte[] buffer, int offset, long time) {
long seconds = time / 1000L;
long milliseconds = time - seconds * 1000L;
seconds += OFFSET_1900_TO_1970;
// write seconds in big endian format
buffer[offset++] = (byte) (seconds >> 24);
buffer[offset++] = (byte) (seconds >> 16);
buffer[offset++] = (byte) (seconds >> 8);
buffer[offset++] = (byte) (seconds >> 0);
long fraction = milliseconds * 0x100000000L / 1000L;
// write fraction in big endian format
buffer[offset++] = (byte) (fraction >> 24);
buffer[offset++] = (byte) (fraction >> 16);
buffer[offset++] = (byte) (fraction >> 8);
// low order bits should be random data
buffer[offset++] = (byte) (Math.random() * 255.0);
}
}
步骤二:新建一个工具类TimeUtils ,获取当前校准过后的时间(已验证,可直接copy使用)
public class TimeUtils {
private String[] ntpServerPool = {"ntp1.aliyun.com", "ntp.ntsc.ac.cn", "time.google.com", "time.windows.com", "time.nist.gov", "time.apple.com",
"time1.cloud.tencent.com", "time2.apple.com", "time2.google.com", "3.asia.pool.ntp.org"};
//校准时间起始开机时间
private long startRealTime;
private static TimeUtils instance;
//校准后 开始时间
private long startTime;
//是否校准
public boolean isCalibrated = false;
public static TimeUtils getInstance() {
if (null == instance) {
instance = new TimeUtils();
}
return instance;
}
/**
* 返回自启动以来的毫秒数,包括睡眠时间
*
* @return
*/
public long getElapsedRealtime() {
return SystemClock.elapsedRealtime();
}
/**
* 获取时间
*
* @return
*/
public Date getDate() {
long elapsedRealtime = SystemClock.elapsedRealtime();
return startRealTime == 0 ? new Date(System.currentTimeMillis() - SystemClock.elapsedRealtime() + elapsedRealtime)
: new Date(elapsedRealtime - this.startRealTime + startTime);
}
/**
* 使用ntp网络时间与本地时间进行校准
*/
public void checkTime() {
//异步调用获取ntp协议网络时间
new Thread(new Runnable() {
@Override
public void run() {
NTPClient ntpclient = new NTPClient();
for (String ntpHost : ntpServerPool) {
if (ntpclient.requestTime(ntpHost, 30000)) {
long currTime = ntpclient.getNtpTime() + SystemClock.elapsedRealtime() - ntpclient.getNtpTimeReference();
long nowTime = currTime / 1000; //当前获取到的准确ntp时间戳(秒单位)
startRealTime = SystemClock.elapsedRealtime();
startTime = System.currentTimeMillis() + ntpclient.getTimeOffset();
isCalibrated = true; //为true代表当前已经拉到网络ntp
break;
}
}
}
}).start();
}
}
步骤三:接口调用实现(已验证,可直接copy使用)
//1、在你初始化或者在你需要初始化获取ntp网络时间的地方调用一次即可,以下举例子:
public void init(Context context){
TimeUtils.getInstance().checkTime();
}
//2、在你想要获取准确时间处调用以下代码,获取当前已校准的用户时间(实时)
long cunrrent_time = TimeUtils.getInstance().getDate().getTime();//单位为毫秒,如需到秒需除以1000
完结:总之,经过自动化以及各种环境测试,无论用户怎么更改本地时间或者日期更改多往前往后此方案都可以解决。