转自http://zhengken.me/2016/09/26/the-principle-of-date-time-sync/#more

前言

在 Android 手机中,我们打开设置可以看到自动确定时间和时区的功能,有时候我故意把手机网络关闭,但时间和时区的设置依然有效,总能把一个错误的时间或时区设置成当前正确的时间,这到底是为什么呢?看完这篇文章,相信你能找到答案。

结论

在分析之前,先把结论说了吧。Android 时间同步有两种方式,分别是从运营商网络获取时间,其中运营商提供时间和时区,用的是 NITZ 协议,网络只能提供时间,用的是 SNTP 协议(NTP 协议的简单版本)。所以,我们的手机会通过营运商和网络两种手段来获取时间,从我的分析来看,如果手机已经从运营商获取时间了,那么就不会从网络获取时间了。

NITZ, or Network Identity and Time Zone, is a mechanism for provisioning local time and date, time zone and DST offset, as well as network provider identity information, to mobile devices via a wireless network.

NTP, Network Time Protocol is a networking protocol for clock synchronization between computer systems over packet-switched, variable-latency data networks.

SNTP, A less complex implementation of NTP, using the same protocol but without requiring the storage of state over extended periods of time, is known as the Simple Network Time Protocol.

结论验证视频


未插 sim 卡,从网络获取时间,验证无法改变时区,但能准确的改变时间


插入 sim 卡,但未连接网络,验证从运营商能获取时间和时区

源码分析

Android 设置入口

DateTimeSettings.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (preference.getKey().equals(KEY_AUTO_TIME)) {
boolean autoEnabled = (Boolean) newValue;
Settings.Global.putInt(getContentResolver(), Settings.Global.AUTO_TIME,
autoEnabled ? 1 : 0);
mTimePref.setEnabled(!autoEnabled);
mDatePref.setEnabled(!autoEnabled);
} else if (preference.getKey().equals(KEY_AUTO_TIME_ZONE)) {
boolean autoZoneEnabled = (Boolean) newValue;
Settings.Global.putInt(
getContentResolver(), Settings.Global.AUTO_TIME_ZONE, autoZoneEnabled ? 1 : 0);
mTimeZone.setEnabled(!autoZoneEnabled);
}
return true;
}

当我们选中自动更新时间,就会向Settings.Global.AUTO_TIME写入 1,此刻,ContentProvider中的值变化了,如果在其他地方对此处的内容变化有监听的话,势必会执行其他地方相应的函数。

将运营商时间应用于手机中

GsmServiceStateTracker.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private ContentObserver mAutoTimeObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
Rlog.i("GsmServiceStateTracker", "Auto time state changed");
revertToNitzTime();
}
};
private ContentObserver mAutoTimeZoneObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
Rlog.i("GsmServiceStateTracker", "Auto time zone state changed");
revertToNitzTimeZone();
}
};
mCr.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
mAutoTimeObserver);
mCr.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
mAutoTimeZoneObserver);

上面的代码是监听设置入口内容变化的,我们可以看到内容变化后执行的操作是revertToNitzTime()revertToNitzTimeZone(), 这两个函数类似,我们这里只分析前者。

1
2
3
4
5
6
7
private void revertToNitzTime() {
......
if (mSavedTime != 0 && mSavedAtTime != 0) {
setAndBroadcastNetworkSetTime(mSavedTime
+ (SystemClock.elapsedRealtime() - mSavedAtTime));
}
}

revertToNitzTime()执行的是setAndBroadcastNetworkSetTime,这里我们先不考虑mSavedTimemSavedAtTime,我们把注意力转向setAndBroadcastNetworkSetTime

1
2
3
4
5
6
7
8
private void setAndBroadcastNetworkSetTime(long time) {
if (DBG) log("setAndBroadcastNetworkSetTime: time=" + time + "ms");
SystemClock.setCurrentTimeMillis(time);
Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME);
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
intent.putExtra("time", time);
mPhone.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}

该函数做了两件事情,第一件是设置了系统的时间,第二件是发送了一个广播。
现在我来解释一下setAndBroadcastNetworkSetTime(mSavedTime + (SystemClock.elapsedRealtime() - mSavedAtTime))中参数的含义。mSavedTime是从运营商获取的时间,mSavedAtTime表示从运营商获取时间的时候,手机从启动到现在所经过时间,值类型是SystemClock.elapsedRealtime()。通过下图,我们很清楚的知道mSavedTime + (SystemClock.elapsedRealtime() - mSavedAtTime)就是手机当前的时间。

广播发往何处?

在上一步中,setAndBroadcastNetworkSetTime发出了一个TelephonyIntents.ACTION_NETWORK_SET_TIME的广播,从字面上理解,广播的意义就是通过网络设置系统。通过 Google 我们可以找到 NetworkTimeUpdateService.java 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void registerForTelephonyIntents() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TelephonyIntents.ACTION_NETWORK_SET_TIME);
intentFilter.addAction(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE);
mContext.registerReceiver(mNitzReceiver, intentFilter);
}
private BroadcastReceiver mNitzReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (TelephonyIntents.ACTION_NETWORK_SET_TIME.equals(action)) {
mNitzTimeSetTime = SystemClock.elapsedRealtime();
} else if (TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE.equals(action)) {
mNitzZoneSetTime = SystemClock.elapsedRealtime();
}
}
};

我们可以看到,收到广播之后的操作不过是对一个成员变量进行赋值而已,其他并没有变化,那么我们在找找源码有没有其他广播接收器或者相关代码。

1
2
3
4
5
6
7
8
9
10
/** Observer to watch for changes to the AUTO_TIME setting */
private static class SettingsObserver extends ContentObserver {
......
void observe(Context context) {
ContentResolver resolver = context.getContentResolver();
resolver.registerContentObserver(Settings.Global.getUriFor(Settings.Global.AUTO_TIME),
false, this);
}
......
}

从代码中我们看到,此处对Settings.Global.AUTO_TIME内容变化也有监听,我们继续找代码。

1
2
3
4
5
6
7
8
9
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_AUTO_TIME_CHANGED:
case EVENT_POLL_NETWORK_TIME:
case EVENT_NETWORK_CONNECTED:
onPollNetworkTime(msg.what);
break;
}
}

Settings.Global.AUTO_TIME内容变化时,发送EVENT_AUTO_TIME_CHANGEDhandler处理,最终的执行的函数是onPollNetworkTime(),好的,我们继续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private void onPollNetworkTime(int event) {
// If Automatic time is not set, don't bother.
if (!isAutomaticTimeRequested()) return;
final long refTime = SystemClock.elapsedRealtime();
// If NITZ time was received less than POLLING_INTERVAL_MS time ago,
// no need to sync to NTP.
if (mNitzTimeSetTime != NOT_SET && refTime - mNitzTimeSetTime < POLLING_INTERVAL_MS) {
resetAlarm(POLLING_INTERVAL_MS);
return;
}
final long currentTime = System.currentTimeMillis();
if (DBG) Log.d(TAG, "System time = " + currentTime);
// Get the NTP time
if (mLastNtpFetchTime == NOT_SET || refTime >= mLastNtpFetchTime + POLLING_INTERVAL_MS
|| event == EVENT_AUTO_TIME_CHANGED) {
if (DBG) Log.d(TAG, "Before Ntp fetch");
// force refresh NTP cache when outdated
if (mTime.getCacheAge() >= POLLING_INTERVAL_MS) {
mTime.forceRefresh();
}
// only update when NTP time is fresh
if (mTime.getCacheAge() < POLLING_INTERVAL_MS) {
final long ntp = mTime.currentTimeMillis();
mTryAgainCounter = 0;
// If the clock is more than N seconds off or this is the first time it's been
// fetched since boot, set the current time.
if (Math.abs(ntp - currentTime) > TIME_ERROR_THRESHOLD_MS
|| mLastNtpFetchTime == NOT_SET) {
// Set the system time
if (DBG && mLastNtpFetchTime == NOT_SET
&& Math.abs(ntp - currentTime) <= TIME_ERROR_THRESHOLD_MS) {
Log.d(TAG, "For initial setup, rtc = " + currentTime);
}
if (DBG) Log.d(TAG, "Ntp time to be set = " + ntp);
// Make sure we don't overflow, since it's going to be converted to an int
if (ntp / 1000 < Integer.MAX_VALUE) {
SystemClock.setCurrentTimeMillis(ntp);
}
} else {
if (DBG) Log.d(TAG, "Ntp time is close enough = " + ntp);
}
mLastNtpFetchTime = SystemClock.elapsedRealtime();
} else {
// Try again shortly
mTryAgainCounter++;
if (mTryAgainCounter <= TRY_AGAIN_TIMES_MAX) {
resetAlarm(POLLING_INTERVAL_SHORTER_MS);
} else {
// Try much later
mTryAgainCounter = 0;
resetAlarm(POLLING_INTERVAL_MS);
}
return;
}
}
resetAlarm(POLLING_INTERVAL_MS);
}

这个函数比较长,那么我来逐句的进行解释一下,PS:该函数的环境是android-4.2.2_r1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//mNitzTimeSetTime 手机设置运营商时间的时刻
//refTime 手机当前时刻
//POLLING_INTERVAL_MS 24L * 60 * 60 * 1000; // 24 hrs
//所以在24小时内已经设置了运营商时间了,那么不设置网络时间
final long refTime = SystemClock.elapsedRealtime();
// If NITZ time was received less than POLLING_INTERVAL_MS time ago,
// no need to sync to NTP.
if (mNitzTimeSetTime != NOT_SET && refTime - mNitzTimeSetTime < POLLING_INTERVAL_MS) {
resetAlarm(POLLING_INTERVAL_MS);
return;
}
//如果网络时间的缓存超过了一天,那么重新获取网络时间,具体的怎么获取在介绍完这个函数再谈。
// force refresh NTP cache when outdated
if (mTime.getCacheAge() >= POLLING_INTERVAL_MS) {
mTime.forceRefresh();
}
// 抽出了 if 语句中的核心代码,ntp 为网络时间缓存中的时间,当 ntp 和本地时间的误差大于 TIME_ERROR_THRESHOLD_MS = 5000 时,设置时间。
if (mTime.getCacheAge() < POLLING_INTERVAL_MS) {
final long ntp = mTime.currentTimeMillis();
if (Math.abs(ntp - currentTime) > TIME_ERROR_THRESHOLD_MS
|| mLastNtpFetchTime == NOT_SET) {
if (ntp / 1000 < Integer.MAX_VALUE) {
SystemClock.setCurrentTimeMillis(ntp);
}
}
//网络时间更新失败,进行定时重新更新
else {
// Try again shortly
mTryAgainCounter++;
if (mTryAgainCounter <= TRY_AGAIN_TIMES_MAX) {
resetAlarm(POLLING_INTERVAL_SHORTER_MS);
} else {
// Try much later
mTryAgainCounter = 0;
resetAlarm(POLLING_INTERVAL_MS);
}
return;

在上面我们看到了获取网络时间的关键代码是mTime.forceRefresh(),我们找到mTime的定义mTime = NtpTrustedTime.getInstance(context),顺藤摸瓜,找NtpTrustedTime。我们就直接跳到网络请求的关键代码吧。

1
2
3
4
5
6
7
8
9
10
final SntpClient client = new SntpClient();
if (client.requestTime(mServer, (int) mTimeout)) {
mHasCache = true;
mCachedNtpTime = client.getNtpTime();
mCachedNtpElapsedRealtime = client.getNtpTimeReference();
mCachedNtpCertainty = client.getRoundTripTime() / 2;
return true;
} else {
return false;
}

我们在文章的开头就介绍了SNTP,是一种简单的NTP协议,误差要比NTP大一点,Android帮我们封装了SntpClient,所以我们要自己获取网络时间的话,就可以直接拿来用了。这边我比较好奇的是Android从哪个服务器获取的时间。defaultServer = res.getString(com.android.internal.R.string.config_ntpServer),我们再找对应的字符串config.xml。

1
2
<!-- Remote server that can provide NTP responses. -->
<string translatable="false" name="config_ntpServer">2.android.pool.ntp.org</string>

服务器地址是2.android.pool.ntp.org,我ping了一下,一共有四个服务器可以使用,分别是:

0.android.pool.ntp.org
1.android.pool.ntp.org
2.android.pool.ntp.org
3.android.pool.ntp.org

除了这几个服务器之外,在www.ntp.org上面,大家还可以找到很多个服务器地址,使用哪个,就自己权衡一下了。

小结

本文只是对时间同步做了一个很表层的分析而已,如果大家对文章哪一部分有疑问,可以给我留言或者自己查看源码,文章中如果有错误的话,也恳请大家指正。

参考文献

  1. http://blog.csdn.net/lindir/article/details/7973700
  2. https://android.googlesource.com/platform/packages/apps/Settings/+/master/src/com/android/settings/DateTimeSettings.java
  3. https://android.googlesource.com/platform/frameworks/opt/telephony/+/jb-mr2-dev/src/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java
  4. https://android.googlesource.com/platform/frameworks/base.git/+/android-4.2.2_r1/services/java/com/android/server/NetworkTimeUpdateService.java
  5. https://android.googlesource.com/platform/frameworks/base/+/android-4.4_r1/core/java/android/util/TrustedTime.java
  6. https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/util/NtpTrustedTime.java
  7. https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml
  8. https://android.googlesource.com/platform/frameworks/opt/telephony/+/tools_r22/src/java/com/android/internal/telephony/RIL.java
  9. http://blog.csdn.net/droyon/article/details/52211132

Android 时间同步原理分析相关推荐

  1. AD时间同步原理分析

    AD时间同步原理分析 对于加入域环境的客户端是与在父域中的权威服务器进行时间同步的.默认的同步时间的方法就是使用域层次,客户端会使用其所连接域中的域控来同步时间,而域控会反过来从整个林中的权威时间源来 ...

  2. android 实例源码解释,Android Handler 原理分析及实例代码

    Android Handler 原理分析 Handler一个让无数android开发者头疼的东西,希望我今天这边文章能为您彻底根治这个问题 今天就为大家详细剖析下Handler的原理 Handler使 ...

  3. Android LayoutInflater原理分析,带你一步步深入了解View

    Android视图绘制流程完全解析,带你一步步深入了解View(一) 转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/12921889 ...

  4. Android JNI原理分析

    引言:分析Android源码6.0的过程,一定离不开Java与C/C++代码直接的来回跳转,那么就很有必要掌握JNI,这是链接Java层和Native层的桥梁,本文涉及相关源码: frameworks ...

  5. android消除锯齿原理分析

    原文地址:https://blog.csdn.net/Apple_hsp/article/details/50833300 前言 在Android中view绘画是很重要的一点,当view重写.surf ...

  6. Android LayoutInflater原理分析,带你一步步深入了解View(一)

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/12921889 有段时间没写博客了,感觉都有些生疏了呢.最近繁忙的工作终于告一段落, ...

  7. android 补间动画有停顿,Android动画原理分析(一)----补间动画

    1.基本特点 补间动画(Tween动画),是android最早的动画框架,从Android1.0开始就有. 功能:可以实现移动.旋转.缩放.渐变四种效果以及这四种效果的组合形式. 实现形式:xml和代 ...

  8. Android LayoutInflater原理分析,带你一步步深入了解View(一) 郭霖学习摘要

    2019独角兽企业重金招聘Python工程师标准>>> public class MainActivity extends Activity {//----------------- ...

  9. 第十八期 Android GPS原理分析《手机就是开发板》

    如果想对Android的整个框架有一个更深层次的感性认识,我们还需要抓住一个点去研究一下.Android按照模块分成很多个系统,比如Audio,Video Out,Camera,Phone,WIFI, ...

最新文章

  1. matlab中find函数使用
  2. java中表示二进制、八进制、十进制、十六进制,double、float、整型
  3. 需求管理(3)------方法论
  4. MATLAB实现偏最小二乘回归PLS
  5. _Linux 最常用命令整理,建议收藏!
  6. Java容器---Set: HashSet TreeSet LinkedHashSet
  7. 说说计算机发展史在你印象里都有哪些内容,《老王》导学案及答案
  8. unity最基本操作
  9. javascript中引号嵌套
  10. 【C++】 53_被遗弃的多重继承 (上)
  11. html为民间 图标不见了,win7 电脑右下角的图标不见了 怎么弄
  12. Qt 简单截图工具(一) 高仿QQ截屏 滑动截屏
  13. Java 案例:珠穆朗玛峰的高度
  14. 2022美国小非农ADP数据发布时间一览表
  15. 模板引擎不关心内容之——art-template,碰见的同步与fs.readFile异步以及函数回调问题的描述,针对fs的readfille读取文件时,返回不了异步函数返回值的解决方法
  16. 使用C#通过串口控制IT6333B电流源
  17. Zabbix监控流程和web界面功能
  18. SCA可达性分析基础知识普及
  19. 使用集集快速添加公众号预约功能
  20. 产品经理不能做错的5件事

热门文章

  1. python爬取电影网站存储于数据库_python爬虫 猫眼电影和电影天堂数据csv和mysql存储过程解析...
  2. MYSQL查询一年中12个月的数据,补全12个月.
  3. 二进制数与十进制数相互转换的C代码(C语言/C程序)
  4. python图片定位_[求助贴]python图片全屏截取+定位坐标+图片识别
  5. [电商直播策划]吸粉卖货两不误,给你一整完整的直播卖货策划全方案!
  6. mysql 开放远程访问
  7. Omni Recover适用的IOS数据恢复方案
  8. jQuery总结四、append 与appendTo、after、before、wrap、unwrap、replaceWith、empty()、remove;detach、is、hasclass
  9. linux下进行图片压缩(pdf转换为jpg)
  10. jni 调用java接口_JNI 调用 JAVA 接口