问题现象

最近处理了一个非常有意思的体系bug,修正体系时刻,重启后居然没有收效

Android时间设置的三个小彩蛋

留意要封闭运用网络供给的时刻运用网络供给的时区这两个开关。

重启后显现的时刻日期为

Android时间设置的三个小彩蛋

显现的时刻既不是我设置的时刻,也不是当时时刻(当时时刻为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;
    }
    //省掉部分代码
}

mHostDatePreferenceHost接口,接口完结在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然后显现,SettingsDialogFragmentSettingsPreferenceFragment的一个内部类,看一下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。

Android时间设置的三个小彩蛋

看一下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。

Android时间设置的三个小彩蛋

挑选完时刻后,最后经过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,重启后的日志如下

Android时间设置的三个小彩蛋

四个值分别为:

  • 体系编译时刻:1669271830000,格式化后为2022-11-24 14:37:10
  • 根目录最近修正时刻:1678865533000,格式化后为2023-03-15 15:32:13
  • 构建时刻:1669271830000,同体系编译时刻
  • 设置的时刻:1664609754998,格式化后为2022-10-01 15:35:54

留意,咱们只需要留意日期,不需要关注时分秒,能够看到四个时刻傍边,最大的为根目录最近修正时刻,所以终究显现的日期为2023-03-15,此为彩蛋3。

Android时间设置的三个小彩蛋

我在开发板和小米手机上测验的结果相同,阐明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是不是能够在这一块优化一下,直接设置里边告诉用户我能设置的时刻区域岂不是更人性化,毕竟细节决定成败。