赞
踩
最近负责的车机项目是海外项目,涉及到全球多个地区。应用开发人员在使用时区时遇到一些问题,故本人做了一点学习;本文基于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代码如下:
在系统层编译时,在mk文件中设置系统属性如下,调试时也可以根据查询这个属性确定当前得时区是哪里;
PRODUCT_DEFAULT_PROPERTY_OVERRIDES += persist.sys.timezone=Asia/Shanghai //设置系统默认时区为上海时区
不是所有得城市都支持直接的城市时区设置。当前系统大体支持两种方式设置时区,一种是通过设置城市得到相应得时区,另一种是设置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
...
主要可以通过设置城市或者设置GMT的方式设置时区
AlarmManager alarm;
...
alarm= (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);//获得AlarmManager服务对象
alarm.setTimeZone("Asia/Shanghai");//设置上海时区
alarm.setTimeZone("GMT"); //0时区时间,可以去除夏令时
alarm.setTimeZone("Etc/GMT-8"); //设置东八区的时区
通过广播的方式,系统层通过广播发送,应用层接收:
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); } } }
TimeZone timeZone2 = TimeZone.getDefault();//获得当前时区对象
String timeZoneName = timeZone2.getID();//时区名字
String timeZoneutc = timeZone2.getDisplayName();//时区对应的称呼,比如格林尼治时区,中国标准时区
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");//获取上海时区对象
boolean usesDaylightTime = timeZone.useDaylightTime();//查询当前的城市时区是否使用夏令时
设置时区方法,接口时序图如下;
源码路径:
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;
}
夏令时又称夏季时间(没有冬令时概念)。它是为节约能源而人为规定地方时间的制度(鼓励人们早睡早起,不要浪费电,夏天日照时间长尽量多用自然资源),全球约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数据库。在不方便更新数据库时,可以考虑对这个城市的夏令时时间做二次加工。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。