最近负责的车机项目是海外项目,涉及到全球多个地区。应用开发人员在使用时区时遇到一些问题,故本人做了一点学习;本文基于android9; 可以使用这些网址查询城市的时间时区等信息: https://time.bmcx.com/Chatham_Islands__localtime/ 使用过程中发现在这个夏令时不准 http://www.timeofdate.com/city

目录

模块架构基本方法使用主要方法代码流程

一、模块架构

主要分为三个部分,API层AlarmManager,framework层AlarmManagerService,以及Native层com_android_server_AlarmManagerService;

AlarmManager:封装在framework.jar中,提供给APP调用的接口,会用到一些工具类zoneInfo、TzData等; AlarmManagerService:service层,时区、时间等相关的逻辑处理,也会用到工具类zoneInfo、TimeZone等; com_android_server_AlarmManagerService:Native层,通过JNI被服务层调用;

二、时区基本方法使用

使用比较简单,通过AlarmManager完成对alarm服务的一系列操作。基本的操作主要包括设置时区、监听时区变化、主动查询时区,基本满足应用开发的使用。主要demo代码如下:

2.1 设置系统默认时区

在系统层编译时,在mk文件中设置系统属性如下,调试时也可以根据查询这个属性确定当前得时区是哪里;

PRODUCT_DEFAULT_PROPERTY_OVERRIDES += persist.sys.timezone=Asia/Shanghai //设置系统默认时区为上海时区

2.2 查询系统支持的时区

不是所有得城市都支持直接的城市时区设置。当前系统大体支持两种方式设置时区,一种是通过设置城市得到相应得时区,另一种是设置GMT时区获得到对应时区;如下方法可以获得当前支持设置得时区参数;

String[] availableIDs = TimeZone.getAvailableIDs();

Log.i("luyao", "onClick: bt_setzone可用zoneId总数 ="+availableIDs.length);

for (String zoneId : availableIDs) {

Log.i("luyao", "onClick: bt_setzone= "+zoneId);

}

对应的打印如下

2020-08-15 00:37:26.957 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone可用zoneId总数 =591

2020-08-15 00:37:26.957 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Africa/Abidjan

2020-08-15 00:37:26.957 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Africa/Accra

2020-08-15 00:37:26.957 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Africa/Addis_Ababa

2020-08-15 00:37:26.957 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Africa/Algiers

2020-08-15 00:37:26.957 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Africa/Asmara

2020-08-15 00:37:26.957 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Africa/Asmera

...

2020-08-15 00:37:26.987 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Etc/GMT+0

2020-08-15 00:37:26.988 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Etc/GMT+1

2020-08-15 00:37:26.988 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Etc/GMT+10

2020-08-15 00:37:26.988 12555-12555/com.example.mtktest I/luyao: onClick: bt_setzone= Etc/GMT+11

...

2.3 设置时区

主要可以通过设置城市或者设置GMT的方式设置时区

AlarmManager alarm;

...

alarm= (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);//获得AlarmManager服务对象

alarm.setTimeZone("Asia/Shanghai");//设置上海时区

alarm.setTimeZone("GMT"); //0时区时间,可以去除夏令时

alarm.setTimeZone("Etc/GMT-8"); //设置东八区的时区

2.4 接收时区变化

通过广播的方式,系统层通过广播发送,应用层接收:

private TimeZoneChangeReceiver timeZoneChangeReceiver;

...

timeZoneChangeReceiver = new TimeZoneChangeReceiver();

IntentFilter filter = new IntentFilter(ACTION_TIMEZONE_CHANGED);

registerReceiver(timeZoneChangeReceiver, filter);//注册广播接收器

...

//创建广播接收器

public class TimeZoneChangeReceiver extends BroadcastReceiver {

@Override

public void onReceive(Context context, Intent intent) {

if (intent.getAction().equals(ACTION_TIMEZONE_CHANGED)) {

// 处理时区变化的逻辑

String newTimeZone = intent.getStringExtra("time-zone");

Log.d("luyao", "时区变化:" + newTimeZone);

}

}

}

2.5 查询当前时区

TimeZone timeZone2 = TimeZone.getDefault();//获得当前时区对象

String timeZoneName = timeZone2.getID();//时区名字

String timeZoneutc = timeZone2.getDisplayName();//时区对应的称呼,比如格林尼治时区,中国标准时区

TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");//获取上海时区对象

boolean usesDaylightTime = timeZone.useDaylightTime();//查询当前的城市时区是否使用夏令时

三、主要方法代码流程

setTimeZone

设置时区方法,接口时序图如下; 源码路径: frameworks/base/core/java/android/app/AlarmManager.java frameworks/base/services/core/java/com/android/server/AlarmManagerService.java frameworks/base/services/core/jni/com_android_server_AlarmManagerService.cpp libcore/luni/src/main/java/libcore/util/ZoneInfo.java libcore/luni/src/main/java/libcore/util/ZoneInfoDB.java libcore/ojluni/src/main/java/java/time/ZoneId.java

首先暴露给到应用使用的接口setTimeZone(String timeZone) ,参数timeZone的格式为:大洲/城市名,例如Asia/Shanghai; 源码路径: frameworks/base/core/java/android/app/AlarmManager.java

public void setTimeZone(String timeZone) {

if (TextUtils.isEmpty(timeZone)) {

return;

}

// Reject this timezone if it isn't an Olson zone we recognize.

if (mTargetSdkVersion >= Build.VERSION_CODES.M) {

boolean hasTimeZone = false;

try {

hasTimeZone = ZoneInfoDB.getInstance().hasTimeZone(timeZone);//判断设置的城市是否在时区数据库中,是否支持设置

} catch (IOException ignored) {

}

if (!hasTimeZone) {

throw new IllegalArgumentException("Timezone: " + timeZone + " is not an Olson ID");

}

}

try {

mService.setTimeZone(timeZone);

} catch (RemoteException ex) {

throw ex.rethrowFromSystemServer();

}

}

关键方法,判断是否是支持的城市:ZoneInfoDB.getInstance().hasTimeZone(timeZone),如果是支持的城市继续调用AlarmManagerService的setTimeZone方法; ZoneInfoDB源码路径: libcore/luni/src/main/java/libcore/util/ZoneInfo.java

public boolean hasTimeZone(String id) throws IOException {

checkNotClosed();

return cache.get(id) != null;//返回是否对应城市是否存在TimeZone对象

}

AlarmManagerService源码路径: frameworks/base/services/core/java/com/android/server/AlarmManagerService.java

@Override

public void setTimeZone(String tz) {

...

setTimeZoneImpl(tz);

...

}

...

void setTimeZoneImpl(String tz) {

...

TimeZone zone = TimeZone.getTimeZone(tz);//获得对应timezone对象

// Prevent reentrant calls from stepping on each other when writing

// the time zone property

boolean timeZoneWasChanged = false;

synchronized (this) {

String current = SystemProperties.get(TIMEZONE_PROPERTY);

if (current == null || !current.equals(zone.getID())) {

//if (localLOGV) {

Slog.i(TAG, "timezone changed: " + current + ", new=" + zone.getID());

//}

timeZoneWasChanged = true;

SystemProperties.set(TIMEZONE_PROPERTY, zone.getID());

}

// Update the kernel timezone information

// Kernel tracks time offsets as 'minutes west of GMT'

int gmtOffset = zone.getOffset(System.currentTimeMillis());//查询当前时间和对应时区的时间的偏差值,是个时间戳,单位是毫秒 1000*60*60*24

setKernelTimezone(mNativeData, -(gmtOffset / 60000));//把需要修改的时间设置给到内核

}

TimeZone.setDefault(null);

if (timeZoneWasChanged) {//时区如果变化,广播出去

Intent intent = new Intent(Intent.ACTION_TIMEZONE_CHANGED);

intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING

| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND

| Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);

intent.putExtra("time-zone", zone.getID());

getContext().sendBroadcastAsUser(intent, UserHandle.ALL);

}

}

上面方法实现中主要调用了两个方法,TimeZone.getOffset(System.currentTimeMillis())和setKernelTimezone(mNativeData, -(gmtOffset / 60000)); 其中getoffset用于查询当前时间和对应时区的时间的偏差值,而setKernelTimezone是将需要修改的时间设置给到系统内核,完成系统时间的修改; Timezone源码路径: libcore/ojluni/src/main/java/java/util/TimeZone.java

public int getOffset(long date) {

if (inDaylightTime(new Date(date))) {//判断是否是夏令时,如果是加上夏令时的时间

return getRawOffset() + getDSTSavings();

}

return getRawOffset();

}

inDaylightTime、getDSTSavings方法源码目录: libcore/luni/src/main/java/libcore/util/ZoneInfo.java

@Override public boolean inDaylightTime(Date time) {

long when = time.getTime();

int offsetIndex = findOffsetIndexForTimeInMilliseconds(when);//查找此时间在数组中的index

if (offsetIndex == -1) { //未查到,说明无夏令时

// Assume that all times before our first transition are non-daylight.

// Transition data tends to start with a transition to daylight, so just

// copying the first transition would assume the opposite.

// http://code.google.com/p/android/issues/detail?id=14395

return false;

}

return mIsDsts[offsetIndex] == 1;//此数据库保存是否支持夏令时

}

public int getDSTSavings() {

if (useDaylightTime()) {

return 3600000;//一个小时

}

return 0;

}

四、夏令时DST

夏令时又称夏季时间(没有冬令时概念)。它是为节约能源而人为规定地方时间的制度(鼓励人们早睡早起,不要浪费电,夏天日照时间长尽量多用自然资源),全球约40%的国家在夏季使用夏令时,其他国家则全年只使用标准时间。正在使用夏令时的代表国家:美国、欧盟、俄罗斯等等。 夏令时设置的原理: 通过改变时区和时间偏移来实现的。当夏令时开始时,系统会自动将时间偏移增加一小时;当夏令时结束时,系统会自动将时间偏移减少一小时。这样,系统的时钟就会自动调整为夏令时或标准时间。每年的夏令时时间段还不一样(一般在3月的第2个周日开始),比如美国2020年夏令时时间是:2020年3月8日 - 2020年11月1日。具体做法是:在3.8号这天将时钟往前拨拨1个小时,11.1号这天还原回来。 夏令时相关的方法使用如下所示:

alarm.setTimeZone("Australia/Sydney");//设置悉尼时区

TimeZone timeZone2 = TimeZone.getDefault();

String timeZoneName = timeZone2.getID();

Date date = new Date();

Log.d("luyao2", "当前data:" +date.toString());

boolean isdayligt = timeZone2.inDaylightTime(date);//查询当前时间,悉尼是否在使用夏令时

boolean usesDaylightTime = timeZone.useDaylightTime();//查询当前的城市时区是否支持使用夏令时

timeZone2.getDSTSavings();//夏令时的时间偏差是多少

timeZone2.getRawOffset();//UTC的时间偏差是多少

Log.d("luyao2", "当前时区:" + timeZoneName+ " timeZone2.getDisplayName() "+timeZone2.getDisplayName()+ " isdayligt="+isdayligt +" useDaylightTime():" + timeZone2.useDaylightTime()+" getDSTSavings()"+timeZone2.getDSTSavings()+ " "+timeZone2.getRawOffset());

上面的打印日志如下:

2020-08-15 00:42:47.125 13157-13157/com.example.mtktest D/luyao: 时区变化:Australia/Sydney

2020-08-15 00:42:48.274 13157-13157/com.example.mtktest D/luyao2: 当前data:Sat Aug 15 02:42:48 GMT+10:00 2020

2020-08-15 00:42:48.275 13157-13157/com.example.mtktest D/luyao2: 当前时区:Australia/Sydney timeZone2.getDisplayName() Australian Eastern Standard Time isdayligt=false useDaylightTime():true getDSTSavings()3600000 36000000

Android系统默认支持夏令时策略,所以在做国际多国项目时需要对夏令时做充足的了解。在应用使用setTimeZone(“Asia/Shanghai”);的方式设置时区时,会默认存在夏令时。而使用setTimeZone(“Etc/GMT-8”)的方式设置时区时不会有夏令时的策略存在。夏令时的策略是由android集成的一个数据库tzdata里定义。若发生某城市夏令时不准确时,可以考虑是否时这个数据库太老导致,可以尝试更新这个库。

五、常见问题

城市不支持设置时区 UE团队出的设计图中,需要在设置里提供很多城市的时区设置。但是当前系统的tzdata数据库中仅支持部分城市时区设置,比如需要设置北京时区,但是无法通过setTimeZone(“Asia/Beijing”)这种方式设置,因为数据库中没有这个城市; 部分夏令时不准确 由于数据库太老,可以尝试更新tzdata数据库。在不方便更新数据库时,可以考虑对这个城市的夏令时时间做二次加工。

精彩链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。