Android時間設(shè)置的3個小彩蛋分享
問題現(xiàn)象
最近處理了一個非常有意思的系統(tǒng)bug,修改系統(tǒng)時間,重啟后居然沒有生效

注意要關(guān)閉使用網(wǎng)絡(luò)提供的時間和使用網(wǎng)絡(luò)提供的時區(qū)這兩個開關(guān)。
重啟后顯示的時間日期為

顯示的時間既不是我設(shè)置的時間,也不是當(dāng)前時間(當(dāng)前時間為2023-03-20 15:49),那么顯示的這個時間到底是什么時間呢?
為了弄清楚這個問題,我研究了一下Android設(shè)置時間的邏輯,研究過程中還發(fā)現(xiàn)了一些彩蛋。
源碼分析
首先是設(shè)置時間的邏輯,源碼位于packages/apps/Settings/src/com/android/settings/datetime/DatePreferenceController.java
public class DatePreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {
//省略部分代碼
private final DatePreferenceHost mHost;
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
//點(diǎn)擊日期后處理
if (!TextUtils.equals(preference.getKey(), KEY_DATE)) {
return false;
}
//顯示日期選擇框
mHost.showDatePicker();
return true;
}
//省略部分代碼
}mHost是DatePreferenceHost接口,接口實現(xiàn)在packages/apps/Settings/src/com/android/settings/DateTimeSettings.java中,因此,showDatePicker()的邏輯位于該實現(xiàn)類中
@SearchIndexable
public class DateTimeSettings extends DashboardFragment implements
TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost {
//省略部分代碼
@Override
public void showDatePicker() {
//顯示日期選擇對話框
showDialog(DatePreferenceController.DIALOG_DATEPICKER);
}
//省略部分代碼
}showDialog()定義在父類packages/apps/Settings/src/com/android/settings/SettingsPreferenceFragment.java中
public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceFragment
implements DialogCreatable, HelpResourceProvider, Indexable {
protected void showDialog(int dialogId) {
if (mDialogFragment != null) {
Log.e(TAG, "Old dialog fragment not null!");
}
//創(chuàng)建SettingsDialogFragment并進(jìn)行show
mDialogFragment = SettingsDialogFragment.newInstance(this, dialogId);
mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId));
}
}showDialog()中就是創(chuàng)建了SettingsDialogFragment然后顯示,SettingsDialogFragment是SettingsPreferenceFragment的一個內(nèi)部類,看一下SettingsDialogFragment的定義
public static class SettingsDialogFragment extends InstrumentedDialogFragment {
private static final String KEY_DIALOG_ID = "key_dialog_id";
private static final String KEY_PARENT_FRAGMENT_ID = "key_parent_fragment_id";
private Fragment mParentFragment;
private DialogInterface.OnCancelListener mOnCancelListener;
private DialogInterface.OnDismissListener mOnDismissListener;
public static SettingsDialogFragment newInstance(DialogCreatable fragment, int dialogId) {
if (!(fragment instanceof Fragment)) {
throw new IllegalArgumentException("fragment argument must be an instance of "
+ Fragment.class.getName());
}
final SettingsDialogFragment settingsDialogFragment = new SettingsDialogFragment();
settingsDialogFragment.setParentFragment(fragment);
settingsDialogFragment.setDialogId(dialogId);
return settingsDialogFragment;
}
@Override
public int getMetricsCategory() {
if (mParentFragment == null) {
return Instrumentable.METRICS_CATEGORY_UNKNOWN;
}
final int metricsCategory =
((DialogCreatable) mParentFragment).getDialogMetricsCategory(mDialogId);
if (metricsCategory <= 0) {
throw new IllegalStateException("Dialog must provide a metrics category");
}
return metricsCategory;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mParentFragment != null) {
outState.putInt(KEY_DIALOG_ID, mDialogId);
outState.putInt(KEY_PARENT_FRAGMENT_ID, mParentFragment.getId());
}
}
@Override
public void onStart() {
super.onStart();
if (mParentFragment != null && mParentFragment instanceof SettingsPreferenceFragment) {
((SettingsPreferenceFragment) mParentFragment).onDialogShowing();
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
if (savedInstanceState != null) {
mDialogId = savedInstanceState.getInt(KEY_DIALOG_ID, 0);
mParentFragment = getParentFragment();
int mParentFragmentId = savedInstanceState.getInt(KEY_PARENT_FRAGMENT_ID, -1);
if (mParentFragment == null) {
mParentFragment = getFragmentManager().findFragmentById(mParentFragmentId);
}
if (!(mParentFragment instanceof DialogCreatable)) {
throw new IllegalArgumentException(
(mParentFragment != null
? mParentFragment.getClass().getName()
: mParentFragmentId)
+ " must implement "
+ DialogCreatable.class.getName());
}
// This dialog fragment could be created from non-SettingsPreferenceFragment
if (mParentFragment instanceof SettingsPreferenceFragment) {
// restore mDialogFragment in mParentFragment
((SettingsPreferenceFragment) mParentFragment).mDialogFragment = this;
}
}
//通過DialogCreatable接口剝離了dialog的創(chuàng)建
return ((DialogCreatable) mParentFragment).onCreateDialog(mDialogId);
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
if (mOnCancelListener != null) {
mOnCancelListener.onCancel(dialog);
}
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (mOnDismissListener != null) {
mOnDismissListener.onDismiss(dialog);
}
}
public int getDialogId() {
return mDialogId;
}
@Override
public void onDetach() {
super.onDetach();
// This dialog fragment could be created from non-SettingsPreferenceFragment
if (mParentFragment instanceof SettingsPreferenceFragment) {
// in case the dialog is not explicitly removed by removeDialog()
if (((SettingsPreferenceFragment) mParentFragment).mDialogFragment == this) {
((SettingsPreferenceFragment) mParentFragment).mDialogFragment = null;
}
}
}
private void setParentFragment(DialogCreatable fragment) {
mParentFragment = (Fragment) fragment;
}
private void setDialogId(int dialogId) {
mDialogId = dialogId;
}
}很標(biāo)準(zhǔn)的自定義DialogFragment的模板代碼,核心代碼在onCreateDialog()方法當(dāng)中,但此方法通過DialogCreatable接口剝離了dialog的創(chuàng)建,這里也很好理解,因為不僅有設(shè)置日期的Dialog,還有設(shè)置時間的Dialog,如果寫死的話,那么就需要定義兩個DialogFragment,所以這里它給抽象出來了,DialogCreatable接口的實現(xiàn)仍然在DateTimeSettings當(dāng)中,它的父類SettingsPreferenceFragment實現(xiàn)了DialogCreatable
@SearchIndexable
public class DateTimeSettings extends DashboardFragment implements
TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost {
//省略部分代碼
@Override
public Dialog onCreateDialog(int id) {
//根據(jù)選項創(chuàng)建對應(yīng)的dialog
switch (id) {
case DatePreferenceController.DIALOG_DATEPICKER:
return use(DatePreferenceController.class)
.buildDatePicker(getActivity());
case TimePreferenceController.DIALOG_TIMEPICKER:
return use(TimePreferenceController.class)
.buildTimePicker(getActivity());
default:
throw new IllegalArgumentException();
}
}
//省略部分代碼
}根據(jù)用戶選擇的操作(設(shè)置日期or設(shè)置時間),創(chuàng)建對應(yīng)的dialog,最終的創(chuàng)建過程由DatePreferenceController來完成
public class DatePreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {
//省略部分代碼
public DatePickerDialog buildDatePicker(Activity activity) {
final Calendar calendar = Calendar.getInstance();
//創(chuàng)建DatePickerDialog
final DatePickerDialog d = new DatePickerDialog(
activity,
this,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH));
// The system clock can't represent dates outside this range.
calendar.clear();
calendar.set(2007, Calendar.JANUARY, 1);
//設(shè)置最小時間為2007-01-01
d.getDatePicker().setMinDate(calendar.getTimeInMillis());
calendar.clear();
calendar.set(2037, Calendar.DECEMBER, 31);
//設(shè)置最大時間為2037-12-31
d.getDatePicker().setMaxDate(calendar.getTimeInMillis());
return d;
}
//省略部分代碼
}這里可以看到,系統(tǒng)限制了可選的日期范圍為2007-01-01至2037-12-31,實際操作也確實是這樣子的(開發(fā)板和小米手機(jī)都是),此為彩蛋1。

看一下DatePickerDialog的定義
public class DatePickerDialog extends AlertDialog implements OnClickListener,
OnDateChangedListener {
private static final String YEAR = "year";
private static final String MONTH = "month";
private static final String DAY = "day";
@UnsupportedAppUsage
private final DatePicker mDatePicker;
private OnDateSetListener mDateSetListener;
//省略部分代碼
private DatePickerDialog(@NonNull Context context, @StyleRes int themeResId,
@Nullable OnDateSetListener listener, @Nullable Calendar calendar, int year,
int monthOfYear, int dayOfMonth) {
super(context, resolveDialogTheme(context, themeResId));
final Context themeContext = getContext();
final LayoutInflater inflater = LayoutInflater.from(themeContext);
//初始化Dialog的View
final View view = inflater.inflate(R.layout.date_picker_dialog, null);
setView(view);
setButton(BUTTON_POSITIVE, themeContext.getString(R.string.ok), this);
setButton(BUTTON_NEGATIVE, themeContext.getString(R.string.cancel), this);
setButtonPanelLayoutHint(LAYOUT_HINT_SIDE);
if (calendar != null) {
year = calendar.get(Calendar.YEAR);
monthOfYear = calendar.get(Calendar.MONTH);
dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
}
mDatePicker = (DatePicker) view.findViewById(R.id.datePicker);
mDatePicker.init(year, monthOfYear, dayOfMonth, this);
mDatePicker.setValidationCallback(mValidationCallback);
mDateSetListener = listener;
}
//省略部分代碼
/**
* Sets the listener to call when the user sets the date.
*
* @param listener the listener to call when the user sets the date
*/
public void setOnDateSetListener(@Nullable OnDateSetListener listener) {
mDateSetListener = listener;
}
@Override
public void onClick(@NonNull DialogInterface dialog, int which) {
switch (which) {
case BUTTON_POSITIVE:
if (mDateSetListener != null) {
// Clearing focus forces the dialog to commit any pending
// changes, e.g. typed text in a NumberPicker.
mDatePicker.clearFocus();
//設(shè)置完成回調(diào)
mDateSetListener.onDateSet(mDatePicker, mDatePicker.getYear(),
mDatePicker.getMonth(), mDatePicker.getDayOfMonth());
}
break;
case BUTTON_NEGATIVE:
cancel();
break;
}
}
//省略部分代碼
/**
* The listener used to indicate the user has finished selecting a date.
*/
public interface OnDateSetListener {
/**
* @param view the picker associated with the dialog
* @param year the selected year
* @param month the selected month (0-11 for compatibility with
* {@link Calendar#MONTH})
* @param dayOfMonth the selected day of the month (1-31, depending on
* month)
*/
void onDateSet(DatePicker view, int year, int month, int dayOfMonth);
}
}可以看到也是標(biāo)準(zhǔn)的自定義Dialog,不過它是繼承的AlertDialog,設(shè)置完成后通過OnDateSetListener進(jìn)行回調(diào),而DatePreferenceController實現(xiàn)了該接口
public class DatePreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {
//省略部分代碼
@Override
public void onDateSet(DatePicker view, int year, int month, int day) {
//設(shè)置日期
setDate(year, month, day);
//更新UI
mHost.updateTimeAndDateDisplay(mContext);
}
//省略部分代碼
@VisibleForTesting
void setDate(int year, int month, int day) {
Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, month);
c.set(Calendar.DAY_OF_MONTH, day);
//設(shè)置日期與定義的最小日期取最大值,也就意味著設(shè)置的日期不能小于定義的最小日期
long when = Math.max(c.getTimeInMillis(), DatePreferenceHost.MIN_DATE);
if (when / 1000 < Integer.MAX_VALUE) {
//設(shè)置系統(tǒng)時間
((AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE)).setTime(when);
}
}
}可以看到系統(tǒng)定義了一個最小日期DatePreferenceHost.MIN_DATE,其值為2007-11-05 0:00
public interface UpdateTimeAndDateCallback {
// Minimum time is Nov 5, 2007, 0:00.
long MIN_DATE = 1194220800000L;
void updateTimeAndDateDisplay(Context context);
}最終顯示日期會在目標(biāo)日期和最小日期中取最大值,也就是說設(shè)定的日期不能小于最小日期,而上文說到,選擇的日期范圍為2007-01-01至2037-12-31,因此,如果你設(shè)置的日期在2007-01-01至2007-11-05之間,最終都會顯示2007-11-05,實際測試也是如此(開發(fā)板和小米手機(jī)都是),此為彩蛋2。

選擇完時間后,最后通過AlarmManagerService來設(shè)置系統(tǒng)內(nèi)核的時間,此處涉及到跨進(jìn)程通信,使用的通信方式是AIDL,直接到AlarmManagerService看看如何設(shè)置內(nèi)核時間的
class AlarmManagerService extends SystemService {
//省略部分代碼
/**
* Public-facing binder interface
*/
private final IBinder mService = new IAlarmManager.Stub() {
//省略部分代碼
@Override
public boolean setTime(long millis) {
//先授權(quán)
getContext().enforceCallingOrSelfPermission(
"android.permission.SET_TIME",
"setTime");
//然后設(shè)置系統(tǒng)內(nèi)核時間
return setTimeImpl(millis);
}
//省略部分代碼
}
//省略部分代碼
boolean setTimeImpl(long millis) {
if (!mInjector.isAlarmDriverPresent()) {
Slog.w(TAG, "Not setting time since no alarm driver is available.");
return false;
}
synchronized (mLock) {
final long currentTimeMillis = mInjector.getCurrentTimeMillis();
//設(shè)置系統(tǒng)內(nèi)核時間
mInjector.setKernelTime(millis);
final TimeZone timeZone = TimeZone.getDefault();
final int currentTzOffset = timeZone.getOffset(currentTimeMillis);
final int newTzOffset = timeZone.getOffset(millis);
if (currentTzOffset != newTzOffset) {
Slog.i(TAG, "Timezone offset has changed, updating kernel timezone");
//設(shè)置系統(tǒng)內(nèi)核時區(qū)
mInjector.setKernelTimezone(-(newTzOffset / 60000));
}
// The native implementation of setKernelTime can return -1 even when the kernel
// time was set correctly, so assume setting kernel time was successful and always
// return true.
return true;
}
}
//省略部分代碼
@VisibleForTesting
static class Injector {
//省略部分代碼
void setKernelTime(long millis) {
Log.d("jasonwan", "setKernelTime: "+millis);
if (mNativeData != 0) {
//在native層完成內(nèi)核時間的設(shè)置
AlarmManagerService.setKernelTime(mNativeData, millis);
}
}
//省略部分代碼
}
//native層完成
private static native int setKernelTime(long nativeData, long millis);
private static native int setKernelTimezone(long nativeData, int minuteswest);
//省略部分代碼
}可以看到最終是在native層完成內(nèi)核時間的設(shè)置,這也理所當(dāng)然,畢竟java是應(yīng)用層,觸及不到kernel層。
回到最開始的問題,為啥開機(jī)之后卻不是我們設(shè)置的時間呢,這就要看看開機(jī)之后系統(tǒng)是怎么設(shè)置時間的。同樣在AlarmManagerService里面,因為它是SystemService的子類,所以會隨著開機(jī)啟動而啟動,而Service啟動后必定會執(zhí)行它的生命周期方法,設(shè)置時間的邏輯就是在onStart()生命周期方法里面
class AlarmManagerService extends SystemService {
//省略部分代碼
@Override
public void onStart() {
mInjector.init();
synchronized (mLock) {
//省略部分代碼
// We have to set current TimeZone info to kernel
// because kernel doesn't keep this after reboot
//設(shè)置時區(qū),從SystemProperty中讀取
setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY));
// Ensure that we're booting with a halfway sensible current time. Use the
// most recent of Build.TIME, the root file system's timestamp, and the
// value of the ro.build.date.utc system property (which is in seconds).
//設(shè)置時區(qū)
//先讀取系統(tǒng)編譯時間
long utc = 1000L * SystemProperties.getLong("ro.build.date.utc", -1L);
//再讀取根目錄最近的修改的時間
long lastModified = Environment.getRootDirectory().lastModified();
//然后讀取系統(tǒng)構(gòu)建時間,三個時間取最大值
final long systemBuildTime = Long.max(
utc,
Long.max(lastModified, Build.TIME));
//代碼1
Log.d("jasonwan", "onStart: utc="+utc+", lastModified="+lastModified+", BuildTime="+Build.TIME+", currentTimeMillis="+mInjector.getCurrentTimeMillis());
//設(shè)置的時間小于最大值,則將最大值設(shè)置為系統(tǒng)內(nèi)核的時間,注意,因為我們剛剛已經(jīng)設(shè)置了內(nèi)核時間,所以重啟后通過System.currentTimeMillis()得到的時間戳為我們設(shè)置的時間,此判斷意味著,系統(tǒng)編譯時間、根目錄最近修改時間、系統(tǒng)構(gòu)建時間、設(shè)置的時間,這四者當(dāng)中取最大值作為重啟后的內(nèi)核時間
if (mInjector.getCurrentTimeMillis() < systemBuildTime) {
//這里mInjector.getCurrentTimeMillis()其實就是System.currentTimeMillis()
Slog.i(TAG, "Current time only " + mInjector.getCurrentTimeMillis()
+ ", advancing to build time " + systemBuildTime);
mInjector.setKernelTime(systemBuildTime);
}
//省略部分代碼
}
//省略部分代碼
@VisibleForTesting
static class Injector {
//省略部分代碼
void setKernelTimezone(int minutesWest) {
AlarmManagerService.setKernelTimezone(mNativeData, minutesWest);
}
void setKernelTime(long millis) {
//代碼2
Log.d("jasonwan", "setKernelTime: "+millis);
if (mNativeData != 0) {
AlarmManagerService.setKernelTime(mNativeData, millis);
}
}
//省略部分代碼
long getElapsedRealtime() {
return SystemClock.elapsedRealtime();
}
long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
//省略部分代碼
}
}實踐驗證
根據(jù)源碼分析得知,系統(tǒng)最終會在系統(tǒng)編譯時間、根目錄最近修改時間、系統(tǒng)構(gòu)建時間、設(shè)置的時間,這四者當(dāng)中取最大值作為重啟后的內(nèi)核時間,這里我在代碼1和代碼2處埋下了log,看看四個時間的值分別是多少,以及最終設(shè)置的內(nèi)核時間是多少,我在設(shè)置中手動設(shè)置的日期為2022-10-01,重啟后的日志如下

四個值分別為:
- 系統(tǒng)編譯時間:1669271830000,格式化后為2022-11-24 14:37:10
- 根目錄最近修改時間:1678865533000,格式化后為2023-03-15 15:32:13
- 構(gòu)建時間:1669271830000,同系統(tǒng)編譯時間
- 設(shè)置的時間:1664609754998,格式化后為2022-10-01 15:35:54
注意,我們只需要注意日期,不需要關(guān)注時分秒,可以看到四個時間當(dāng)中,最大的為根目錄最近修改時間,所以最終顯示的日期為2023-03-15,此為彩蛋3。

我在開發(fā)板和小米手機(jī)上測試的結(jié)果相同,說明MIUI保留了這一塊的邏輯,但是MIUI也有一個bug,就是明明我關(guān)閉了使用網(wǎng)絡(luò)提供的時間和使用網(wǎng)絡(luò)提供的時區(qū),它還是給我自動更新了日期和時間,除非開啟飛行模式之后才不自動更新。
同時我們還注意到,系統(tǒng)編譯時間ro.build.date.utc跟系統(tǒng)構(gòu)建時間Build.TIME是相同的,這很好理解,編譯跟構(gòu)建是一個意思,而且Build.TIME的取值其實也來自于ro.build.date.utc
/**
* Information about the current build, extracted from system properties.
*/
public class Build {
//省略部分代碼
/** The time at which the build was produced, given in milliseconds since the UNIX epoch. */
public static final long TIME = getLong("ro.build.date.utc") * 1000;
//省略部分代碼
}我也搞不懂Google為什么要設(shè)計兩個概念,搞得我一開始還去研究這兩個概念的區(qū)別,結(jié)果沒區(qū)別,數(shù)據(jù)源是一樣的,尷尬。
結(jié)論
設(shè)置系統(tǒng)時間必須大于系統(tǒng)編譯時間和根目錄最近修改時間才會生效。
最后我在想,MIUI是不是可以在這一塊優(yōu)化一下,直接設(shè)置里面告訴用戶我能設(shè)置的時間區(qū)域豈不是更人性化,畢竟細(xì)節(jié)決定成敗。
到此這篇關(guān)于Android時間設(shè)置的3個小彩蛋的文章就介紹到這了,更多相關(guān)Android時間設(shè)置彩蛋內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android基于自帶的DownloadManager實現(xiàn)下載功能示例
這篇文章主要介紹了Android基于自帶的DownloadManager實現(xiàn)下載功能,結(jié)合實例形式分析了DownloadManager實現(xiàn)下載功能的具體操作步驟與相關(guān)注意事項,需要的朋友可以參考下2017-08-08
Android ListView在Fragment中的使用示例詳解
這篇文章主要介紹了Android ListView在Fragment中的使用,因為工作一直在用mvvm框架,因此這篇文章是基于mvvm框架寫的,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-09-09
Flutter之自定義Dialog實現(xiàn)版本更新彈窗功能的實現(xiàn)
這篇文章主要介紹了Flutter之自定義Dialog實現(xiàn)版本更新彈窗功能的實現(xiàn),本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07
Android運(yùn)用BroadcastReceiver實現(xiàn)強(qiáng)制下線
本篇文章主要介紹了Android運(yùn)用BroadcastReceiver實現(xiàn)強(qiáng)制下線,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07
flutter 輸入框組件TextField的實現(xiàn)代碼
這篇文章主要介紹了flutter 輸入框組件TextField的實現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07

