<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
最近處理了一個非常有意思的系統bug,修改系統時間,重啟後居然沒有生效
注意要關閉使用網路提供的時間和使用網路提供的時區這兩個開關。
重啟後顯示的時間日期為
顯示的時間既不是我設定的時間,也不是當前時間(當前時間為2023-03-20 15:49),那麼顯示的這個時間到底是什麼時間呢?
為了弄清楚這個問題,我研究了一下Android設定時間的邏輯,研究過程中還發現了一些彩蛋。
首先是設定時間的邏輯,原始碼位於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) { //點選日期後處理 if (!TextUtils.equals(preference.getKey(), KEY_DATE)) { return false; } //顯示日期選擇框 mHost.showDatePicker(); return true; } //省略部分程式碼 }
mHost
是DatePreferenceHost
介面,介面實現在packages/apps/Settings/src/com/android/settings/DateTimeSettings.java
中,因此,showDatePicker()
的邏輯位於該實現類中
@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!"); } //建立SettingsDialogFragment並進行show mDialogFragment = SettingsDialogFragment.newInstance(this, dialogId); mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId)); } }
showDialog()
中就是建立了SettingsDialogFragment
然後顯示,SettingsDialogFragment
是SettingsPreferenceFragment
的一個內部類,看一下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的建立 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; } }
很標準的自定義DialogFragment
的模板程式碼,核心程式碼在onCreateDialog()
方法當中,但此方法通過DialogCreatable
介面剝離了dialog的建立,這裡也很好理解,因為不僅有設定日期的Dialog,還有設定時間的Dialog,如果寫死的話,那麼就需要定義兩個DialogFragment
,所以這裡它給抽象出來了,DialogCreatable
介面的實現仍然在DateTimeSettings
當中,它的父類別SettingsPreferenceFragment
實現了DialogCreatable
@SearchIndexable public class DateTimeSettings extends DashboardFragment implements TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost { //省略部分程式碼 @Override public Dialog onCreateDialog(int id) { //根據選項建立對應的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(); } } //省略部分程式碼 }
根據使用者選擇的操作(設定日期or設定時間),建立對應的dialog,最終的建立過程由DatePreferenceController
來完成
public class DatePreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener { //省略部分程式碼 public DatePickerDialog buildDatePicker(Activity activity) { final Calendar calendar = Calendar.getInstance(); //建立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); //設定最小時間為2007-01-01 d.getDatePicker().setMinDate(calendar.getTimeInMillis()); calendar.clear(); calendar.set(2037, Calendar.DECEMBER, 31); //設定最大時間為2037-12-31 d.getDatePicker().setMaxDate(calendar.getTimeInMillis()); return d; } //省略部分程式碼 }
這裡可以看到,系統限制了可選的日期範圍為2007-01-01至2037-12-31,實際操作也確實是這樣子的(開發板和小米手機都是),此為彩蛋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(); //設定完成回撥 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); } }
可以看到也是標準的自定義Dialog,不過它是繼承的AlertDialog
,設定完成後通過OnDateSetListener
進行回撥,而DatePreferenceController
實現了該介面
public class DatePreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener { //省略部分程式碼 @Override public void onDateSet(DatePicker view, int year, int month, int day) { //設定日期 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); //設定日期與定義的最小日期取最大值,也就意味著設定的日期不能小於定義的最小日期 long when = Math.max(c.getTimeInMillis(), DatePreferenceHost.MIN_DATE); if (when / 1000 < Integer.MAX_VALUE) { //設定系統時間 ((AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE)).setTime(when); } } }
可以看到系統定義了一個最小日期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); }
最終顯示日期會在目標日期和最小日期中取最大值,也就是說設定的日期不能小於最小日期,而上文說到,選擇的日期範圍為2007-01-01至2037-12-31,因此,如果你設定的日期在2007-01-01至2007-11-05之間,最終都會顯示2007-11-05,實際測試也是如此(開發板和小米手機都是),此為彩蛋2。
選擇完時間後,最後通過AlarmManagerService
來設定系統核心的時間,此處涉及到跨程序通訊,使用的通訊方式是AIDL,直接到AlarmManagerService
看看如何設定核心時間的
class AlarmManagerService extends SystemService { //省略部分程式碼 /** * Public-facing binder interface */ private final IBinder mService = new IAlarmManager.Stub() { //省略部分程式碼 @Override public boolean setTime(long millis) { //先授權 getContext().enforceCallingOrSelfPermission( "android.permission.SET_TIME", "setTime"); //然後設定系統核心時間 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(); //設定系統核心時間 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"); //設定系統核心時區 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層完成核心時間的設定 AlarmManagerService.setKernelTime(mNativeData, millis); } } //省略部分程式碼 } //native層完成 private static native int setKernelTime(long nativeData, long millis); private static native int setKernelTimezone(long nativeData, int minuteswest); //省略部分程式碼 }
可以看到最終是在native層完成核心時間的設定,這也理所當然,畢竟java是應用層,觸及不到kernel層。
回到最開始的問題,為啥開機之後卻不是我們設定的時間呢,這就要看看開機之後系統是怎麼設定時間的。同樣在AlarmManagerService
裡面,因為它是SystemService
的子類,所以會隨著開機啟動而啟動,而Service啟動後必定會執行它的生命週期方法,設定時間的邏輯就是在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 //設定時區,從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). //設定時區 //先讀取系統編譯時間 long utc = 1000L * SystemProperties.getLong("ro.build.date.utc", -1L); //再讀取根目錄最近的修改的時間 long lastModified = Environment.getRootDirectory().lastModified(); //然後讀取系統構建時間,三個時間取最大值 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()); //設定的時間小於最大值,則將最大值設定為系統核心的時間,注意,因為我們剛剛已經設定了核心時間,所以重啟後通過System.currentTimeMillis()得到的時間戳為我們設定的時間,此判斷意味著,系統編譯時間、根目錄最近修改時間、系統構建時間、設定的時間,這四者當中取最大值作為重啟後的核心時間 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(); } //省略部分程式碼 } }
根據原始碼分析得知,系統最終會在系統編譯時間、根目錄最近修改時間、系統構建時間、設定的時間,這四者當中取最大值作為重啟後的核心時間,這裡我在程式碼1和程式碼2處埋下了log,看看四個時間的值分別是多少,以及最終設定的核心時間是多少,我在設定中手動設定的日期為2022-10-01,重啟後的紀錄檔如下
四個值分別為:
注意,我們只需要注意日期,不需要關注時分秒,可以看到四個時間當中,最大的為根目錄最近修改時間,所以最終顯示的日期為2023-03-15,此為彩蛋3。
我在開發板和小米手機上測試的結果相同,說明MIUI保留了這一塊的邏輯,但是MIUI也有一個bug,就是明明我關閉了使用網路提供的時間和使用網路提供的時區,它還是給我自動更新了日期和時間,除非開啟飛航模式之後才不自動更新。
同時我們還注意到,系統編譯時間ro.build.date.utc
跟系統構建時間Build.TIME
是相同的,這很好理解,編譯跟構建是一個意思,而且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為什麼要設計兩個概念,搞得我一開始還去研究這兩個概念的區別,結果沒區別,資料來源是一樣的,尷尬。
設定系統時間必須大於系統編譯時間和根目錄最近修改時間才會生效。
最後我在想,MIUI是不是可以在這一塊優化一下,直接設定裡面告訴使用者我能設定的時間區域豈不是更人性化,畢竟細節決定成敗。
到此這篇關於Android時間設定的3個小彩蛋的文章就介紹到這了,更多相關Android時間設定彩蛋內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45