Android 时区使用和总结

最近负责的车机项目是海外项目,涉及到全球多个地区。应用开发人员在使用时区时遇到一些问题,故本人做了一点学习;本文基于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数据库。在不方便更新数据库时,可以考虑对这个城市的夏令时时间做二次加工。

猜你喜欢

转载自blog.csdn.net/u012873121/article/details/132778816