1. 项目整体设计思路:为什么用 Flutter 做 OpenHarmony 应用
做世界时钟这个项目之前,我其实纠结过一阵子:到底是直接用 OpenHarmony 的 ArkUI 写,还是把 Flutter 那一套搬过来。最后选了 Flutter + OpenHarmony 的组合,这里面的考量值得说清楚。
先说结论:如果你的目标设备只是 OpenHarmony 单平台,ArkUI 当然是“正统”选择,性能和系统能力调用最直接。但如果你手里已经有 Flutter 的业务代码、组件库、或者团队本身就是 Flutter 技术栈,那 OpenHarmony 官方维护的 Flutter 引擎(OpenHarmony SIG 组在推进的 flutter_flutter 与 ohos 适配分支)已经能让你把 Dart 代码以极低的成本跑在 OpenHarmony 设备上。世界时钟这种应用,逻辑层全部在 Dart 里,UI 层用 Flutter 的 CustomPainter 和动画系统做高保真渲染,天然适合跨端复用。
从项目形态上看,世界时钟 App 的核心痛点不在业务复杂度,而在三个方面:时间计算的准确性、UI 绘制的精细度、交互切换的流畅性。这三个点 Flutter 都有成熟方案。尤其是模拟指针时钟,完全可以用 CustomPainter 逐帧绘制,不依赖任何第三方图片资源,真正做到“保真”——因为指针角度是由当前时分秒实时计算出来的,精度可以去到毫秒级,这比贴图轮换的方案高级得多。
架构上我分了三层:
- 数据层:城市列表、时区偏移、UTC 时间基准。
- 逻辑层:时间同步引擎、搜索索引、时区计算、模式切换状态机。
- UI 层:模拟时钟画布、数字时钟组件、城市列表与搜索交互、设置面板。
这个拆分的好处是,时间同步引擎和时区计算完全不感知 UI,将来你要加个闹钟模块或者移植到 iOS/Android,逻辑层可以原封不动带走。UI 层也做了组件化,模拟时钟和数字时钟是两套独立组件,通过一个 ClockMode 枚举切换,互不干扰。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与工程搭建:OpenHarmony 上跑 Flutter 的完整步骤
2.1 Flutter SDK 与 OpenHarmony 适配分支的选择
这一步是坑最多的地方。不能用普通的 Flutter stable 版本直接跑 OpenHarmony,必须用适配了 OpenHarmony 的 Flutter SDK。目前社区主流做法是使用 OpenHarmony SIG 维护的 flutter_flutter 仓库中的 ohos 分支。注意版本对齐,不同版本的 OpenHarmony SDK 对应不同版本的 Flutter 适配分支,混用会出现各种链接错误。
我用的组合是:
| 组件 | 版本/分支 |
|---|---|
| OpenHarmony SDK | 4.0 Release(API 10) |
| Flutter SDK | ohos 分支(基于 Flutter 3.7.x 适配) |
| DevEco Studio | 4.0 Release |
| Node.js | 16.x(用于 hdc 和编译脚本) |
安装 Flutter SDK 后,记得把 flutter 和 dart 加到 PATH。然后验证一下:
bash复制flutter doctor
如果你的 Flutter 适配分支正常,flutter doctor 应该能识别到 OpenHarmony 相关的工具链。如果没有识别到,去检查环境变量 OHOS_SDK_HOME 是否指向了 DevEco Studio 的 SDK 目录。
2.2 创建 Flutter 工程并添加 OpenHarmony 平台
Flutter 官方命令默认只有 android、ios、web 等平台目录,OpenHarmony 需要手动添加适配层。方法有两种:
方法一(推荐):直接克隆官方示例工程结构。
bash复制git clone https://github.com/openharmony-sig/flutter_flutter
flutter create --platforms=ohos world_clock_app
如果 --platforms=ohos 不被识别,说明你的 Flutter 分支没有注册 ohos 平台。这时需要手动创建 ohos 目录,并把适配工程放入。
方法二:用 DevEco Studio 创建标准的 OpenHarmony 工程,然后把 Flutter 模块作为依赖集成进去。这种方式更贴近“原生壳 + Flutter 内核”的混合架构,适合需要调用大量系统能力的场景。
我实际开发采用的是方法一,然后手动调整 ohos 目录下的 entry 模块配置。核心要改两个文件:
build-profile.json5:配置签名和模块依赖。entry/src/main/module.json5:配置应用包名、入口 ability。
2.3 hdc 连接与实时调试
OpenHarmony 真机调试用 hdc 命令,类似 Android 的 adb。先确认设备连接:
bash复制hdc list targets
如果没有输出设备,检查 USB 调试是否开启,或者用网络调试:
bash复制hdc tconn 192.168.1.100:5555
查看 OpenHarmony 系统版本,我经常用这个命令确认设备环境是否匹配:
bash复制hdc shell param get const.product.software.version
这个命令很有用,因为 OpenHarmony 不同 API 版本对 Flutter 引擎的接口支持差异很大,很多运行时崩溃都是版本不匹配导致。调试时把日志拉出来看:
bash复制hdc shell hilog
Flutter 侧的 debug 日志会直接走 hilog 输出,关键字过滤 Flutter 即可。
3. 时间实时同步引擎:世界时钟的核心心脏
3.1 时间基准与时区偏移模型
世界时钟的第一需求是“准”。但这儿有个容易被新手忽略的点:世界时钟需要显示的是“某个城市的本地时间”,而不是“UTC 时间 + 固定偏移”那么简单。因为很多国家有夏令时,偏移量会随日期变化。如果只是存一个 utc_offset 字段,你就会在夏令时切换那天看到神奇的错误时间。
我的设计方案是:
- 基准时间:使用设备系统时间(
DateTime.now())作为时间源。虽然精准同步场景需要 NTP,但世界时钟这种应用,设备时钟的精度已经足够,系统会自己校时。 - 城市时区表:每条城市数据包含
IANA 时区名(如Asia/Shanghai、America/New_York),通过DateTime的时区转换能力换算对应城市时间。Flutter 的DateTime不内置 IANA 时区数据库,我用的是timezone包配合tzdata来加载完整时区数据。 - 夏令时处理:
timezone包会自动计算夏令时规则,前提是导入完整的tzdata数据库。这个库体积大概几百 KB,对现代设备不算负担,但换来的是时间计算的绝对正确性。
换算城市时间的核心逻辑:
dart复制import 'package:timezone/timezone.dart' as tz;
DateTime getCityTime(String cityTimezone) {
final location = tz.getLocation(cityTimezone);
return tz.TZDateTime.now(location);
}
TZDateTime.now(location) 返回的就是该时区的当前本地时间,已经包含了夏令时偏移。注意:千万不要自己去算“UTC + 固定偏移”,那一定是错的。
3.2 秒级刷新:Timer 与帧回调的选择
世界时钟的 UI 需要每秒更新一次,但这里有个性能陷阱:如果用 Timer.periodic(Duration(seconds: 1)) 直接触发 setState(),时间会不均匀——因为 Dart 的事件循环受其他任务影响,Timer 回调会漂移。表现就是秒针偶尔跳两格或者停一下。
更好的方案是用 Flutter 的 Ticker(每帧回调)。每帧都去检查当前时间,如果秒数变了才刷新 UI,这样秒针跳动跟系统帧率对齐,视觉上非常平滑。
dart复制class _ClockTick extends StatefulWidget {
@override
State<StatefulWidget> createState() => _ClockTickState();
}
class _ClockTickState extends State<_ClockTick>
with SingleTickerProviderStateMixin {
late Ticker _ticker;
int _lastSecond = -1;
@override
void initState() {
super.initState();
_ticker = createTicker((elapsed) {
final now = DateTime.now();
if (_lastSecond != now.second) {
setState(() {
_lastSecond = now.second;
});
}
});
_ticker.start();
}
}
用 Ticker 还有一个额外好处:当页面失去焦点时帧回调会自动暂停,省电效果比 Timer 好得多。
3.3 城市列表与搜索索引构建
城市数据源我用的 tzdata 包里的时区列表,再人工补充了城市显示名(中文名 + 英文名 + 国家和地区),数据结构如下:
json复制{
"city": "上海",
"cityEn": "Shanghai",
"country": "中国",
"countryEn": "China",
"timezone": "Asia/Shanghai",
"lat": 31.23,
"lng": 121.47
}
搜索功能要做成“前缀匹配 + 拼音首字母匹配”。中文用户习惯输入“上海”或“sh”来搜。这里我用了一个轻量级拼音库 lpinyin,在构建城市索引时就把城市名、英文名、拼音首字母、拼音全拼都拼成一个搜索字符串:
dart复制String buildSearchKey(Map<String, Object> city) {
final pinyin = PinyinHelper.getPinyin(city['city'], separator: '');
final initials = PinyinHelper.getShortPinyin(city['city']);
return '${city['city']} ${city['cityEn']} '
'${city['country']} ${city['countryEn']} $pinyin $initials'.toLowerCase();
}
用户输入时直接对这个 key 做 contains 匹配,简单高效。城市列表最多几百条,线性扫描完全够用,不需要搞什么复杂搜索引擎。
3.4 实时同步的“准同步”体验
这里聊一个产品层面的设计细节:世界时钟的“实时同步”到底要同步到什么程度?我查过一些模拟器类产品,会发现它们的时间是“每次打开重新 get 一次”,但运行过程中不做持续同步。这样有个问题:如果你把 App 挂在后台两小时再回来,总时间会偏差很大。
我的做法是:每次从后台恢复(AppLifecycleState.resumed)强制同步一次,同时开启 Ticker 把时间拉回当前。这样既保证实时性,也避免频繁的网络校时功耗问题。对于需要绝对精确的时间场景,可以考虑接入 NTP,但就世界时钟来说,本地系统时间偏移通过 Ticker 校正已经足够——用户看到的是秒针在跳,心理上就是“实时”。
4. 高保真时钟 UI:CustomPainter 绘制模拟指针,做到“指哪打哪”
4.1 为什么用 CustomPainter 而不是图片
市面上很多时钟 App 的模拟表盘是直接用设计稿切图,再旋转图片。这个方案的最大问题:图片旋转有锯齿,而且表盘刻度在低分辨率设备上会糊。真实的世界时钟要的是“指针准、刻度清、扫秒顺滑”。
用 CustomPainter 的好处是:所有元素都是矢量绘制,与分辨率无关;指针角度与时间数据直接关联,不存在“图片旋转对不准刻度”的问题;刷新时只需要重绘变化区域,性能可控。
4.2 表盘绘制方案
表盘的绘制分四层:
- 外圈金属质感圆环(用 RadialGradient 模拟高光)
- 60 个刻度线(每分钟一个短线,每小时一个长线)
- 12 个时间数字(自定义字体 + 位置计算)
- 中心轴与指针
关键代码,绘制刻度线:
dart复制void _drawTickMarks(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 12;
final paint = Paint()
..color = Colors.black87
..strokeWidth = 2
..strokeCap = StrokeCap.round;
for (int i = 0; i < 60; i++) {
final angle = i * 6 * pi / 180;
final isHourMark = i % 5 == 0;
final outerRadius = radius - (isHourMark ? 0 : 6);
final innerRadius = radius - (isHourMark ? 18 : 12);
final x1 = center.dx + innerRadius * cos(angle - pi / 2);
final y1 = center.dy + innerRadius * sin(angle - pi / 2);
final x2 = center.dx + outerRadius * cos(angle - pi / 2);
final y2 = center.dy + outerRadius * sin(angle - pi / 2);
canvas.drawLine(Offset(x1, y1), Offset(x2, y2), paint);
}
}
注意角度偏移了 pi / 2,因为 12 点在画布的顶部,而三角函数从 0 度开始是三点钟方向。这个细节不处理,表盘会歪 90 度,我第一次做就踩了这个坑。
4.3 指针角度计算:时针不是简单 hour * 30
很多新手会把时针角度算成 hour * 30(每个小时 30 度)。但真实时钟的时针是“连续移动”的,3点半的时候时针应该在 3 和 4 中间,而不是停留在 3。正确算法:
dart复制double hourAngle = (hour % 12) * 30 + minute * 0.5 + second * (0.5 / 60);
double minuteAngle = minute * 6 + second * 0.1;
double secondAngle = second * 6;
分针在走的时候也在微动,秒针走一格,分针动 0.1 度,这才是高保真。如果做扫秒(sweep second)效果,秒针角度直接用毫秒参与计算:
dart复制double sweepSecondAngle = (second + millisecond / 1000) * 6;
这个连续角度值,配合 Ticker 刷新,实现的就是真实机械表的“扫秒”质感。比较考验渲染性能,但 CustomPainter 绘制一条线完全没问题。
4.4 模拟/数字切换:动画过渡而不是生硬切换
用户切换模式时,如果直接替换组件,视觉上很生硬。我用了一个简单的交叉淡入淡出 + 缩放动画:
dart复制AnimatedSwitcher(
duration: Duration(milliseconds: 400),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: FadeTransition(opacity: animation, child: child),
);
},
child: _isAnalog
? AnalogClockView(time: _currentTime)
: DigitalClockView(time: _currentTime),
)
AnimatedSwitcher 会同时保留新旧两个组件做过渡,注意要给两个组件加上不同的 Key,否则 Flutter 不认为它们发生了切换。我习惯用 ValueKey 或者枚举类型做 key。
4.5 数字时钟的字体和排版细节
数字时钟看起来简单,但“保真”体现在字体选择和排版上。不要用默认字体,我用了 GoogleFonts 的 Roboto Mono 或者等宽数字字体,避免数字宽度变化导致秒位跳动。另外,数字时钟通常带个秒数专用区域,字号比时分小一点,再加一个背景圆角容器形成“电子表”质感。
数字时钟显示格式建议用 HH:mm:ss 24 小时制,但也要支持 12 小时制的设置。这块做成一个设置项,放到 App 设置面板里。
5. 城市搜索与状态管理:从 Provider 到局部刷新
5.1 状态管理选型:Provider 足矣
Flutter 的状态管理方案多如牛毛:Bloc、Riverpod、GetX、Provider。对于世界时钟这种中等复杂度 App,我用的是 Provider。原因是:项目足够简单,ChangeNotifier + Provider 的样板代码最少;团队如果后续要换 Bloc,Provider 的逻辑也是可以平移的;社区资料多,排查问题方便。
核心状态类:
dart复制class ClockState extends ChangeNotifier {
List<CityTime> _selectedCities = [];
ClockMode _mode = ClockMode.analog;
bool _is24Hour = true;
List<CityTime> get selectedCities => _selectedCities;
ClockMode get mode => _mode;
void addCity(CityTime city) {
_selectedCities.add(city);
notifyListeners();
}
void switchMode(ClockMode mode) {
_mode = mode;
notifyListeners();
}
}
页面组件通过 context.watch<ClockState>() 来监听状态变化,只有依赖了对应状态的组件才会重建。这就避免了整个页面无脑 setState 的性能浪费。
5.2 城市搜索页:防抖与结果高亮
搜索页交互上做两个细节:
防抖:用户连续输入时,不要每个字符都触发搜索。用 Timer 做 300ms 防抖:
dart复制Timer? _debounce;
void onSearchTextChanged(String query) {
_debounce?.cancel();
_debounce = Timer(Duration(milliseconds: 300), () {
_performSearch(query);
});
}
城市数据量小,防抖主要是控制 UI 刷新频率,避免键入过程中列表闪动。
搜索关键词高亮:匹配到的位置用不同颜色标出来。这个用 Flutter 的 RichText 或 TextSpan 来实现:
dart复制List<TextSpan> _highlightMatch(String text, String query) {
final lowerText = text.toLowerCase();
final lowerQuery = query.toLowerCase();
final index = lowerText.indexOf(lowerQuery);
if (index == -1) return [TextSpan(text: text)];
return [
TextSpan(text: text.substring(0, index)),
TextSpan(
text: text.substring(index, index + query.length),
style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
),
TextSpan(text: text.substring(index + query.length)),
];
}
5.3 多城市卡片列表与排序
用户添加多个城市后,首页是一个纵向列表。每张卡片显示:城市名、当前时间、时区偏移、一个开关按钮让用户点击切换时钟模式(独立于全局模式)。
这里涉及 Flutter 列表性能优化的问题。常规 ListView.builder 就行,但要注意每张卡片内部的时间刷新不要全量重建列表。我的做法是:卡片时间自己用 Ticker 刷新,而不是依赖父级的 setState。这样每个卡片独立刷新,切换模式或者添加城市时不会引起整个列表闪烁。
6. OpenHarmony 打包与性能优化:踩过的真实坑
6.1 打 HAP 包的过程
OpenHarmony 应用打包生成的是 .hap 文件。用 DevEco Studio 打开工程,Build > Build Hap(s)/APP(s) > Build Hap(s),编译完成后在 entry/build/default/outputs/default/ 目录下找到 hap 包。
命令行打包方式:
bash复制hvigorw assembleHap
打包之前要确认签名配置。OpenHarmony 的签名分调试签名和发布签名,调试签名会自动生成,不需要额外申请。真机安装:
bash复制hdc install entry/build/default/outputs/default/entry-default-signed.hap
卸载命令:
bash复制hdc uninstall com.example.worldclock
6.2 性能优化:帧率、内存与包体积
世界时钟 App 对性能的敏感点在模拟时钟的秒针动画。如果刷新频率过高,低端设备(比如 RK3568 开发板)会掉帧,CPU 占用飙高。我做的优化:
- 局部重绘:CustomPainter 的
shouldRepaint方法尽量返回false,只有当时间变化超过 1 秒时才触发重绘。如果只是毫秒级别变化(扫秒),通过控制 Ticker 的活跃状态来节流。 - 开启硬件加速:OpenHarmony 的 Flutter 引擎默认开了 GPU 渲染,如果发现 CPU 占用高,检查是不是在软件渲染模式下跑。可以在工程配置里启用 GPU 加速。
- 包体积控制:
timezone的tzdata全量数据有 400KB 左右,如果嫌大,可以只加载需要的时区子集。timezone包支持按需加载特定区域的数据文件,但我测试下来,完整 tzdata 对包体积影响不大,直接全量加载省事且不容易漏时区。
6.3 OpenHarmony 上 Flutter 的已知问题
OpenHarmony 对 Flutter 的支持还在快速迭代中,遇到一些坑是正常的。我整理了几个常见问题:
问题一:Flutter 插件无法编译
OpenHarmony 上很多 Flutter 插件没有对应适配版本,编译时会报错找不到插件实现。我查了 flutter_flutter 插件的 pubspec,发现 OpenHarmony 采用的是“插件联邦”机制,需要用 flutter pub add 安装支持 ohos 的插件版本。如果插件没适配 openharmony,手动在 ohos 目录下添加一个桥接实现太复杂,不如换一个支持 ohos 的插件。
问题二:日志输出不完整
OpenHarmony 上 Flutter 的 debug 日志有时不打印 stack trace。解决方法是先加一个全局错误捕获,把异常信息写到本地文件:
dart复制void main() {
FlutterError.onError = (details) {
debugPrint(details.toString());
// 同时写入日志文件
};
runApp(MyApp());
}
问题三:输入法弹出导致布局溢出
在 OpenHarmony 上,搜索框弹出输入法时,Flutter 的 Scaffold resizeToAvoidBottomInset 默认行为可能导致 RenderFlex overflowed。我的处理方式:搜索页用 SafeArea 包住,并且设置 resizeToAvoidBottomInset: false,给搜索结果列表加一个 Expanded,确保键盘弹出时列表自适应。
7. 高级体验优化:从能用到“高保真”的细节打磨
7.1 城市卡片的时间差计算
除了每个城市的时间,用户通常还想知道“现在北京时间几点,纽约几点”,以及“自己所在城市和其他城市的时差”。我在卡片上加了“相对于当前城市的时间差”标签:
dart复制String formatTimeDifference(DateTime local, DateTime remote) {
final diff = remote.difference(local).inHours;
if (diff == 0) return '同时区';
final prefix = diff > 0 ? '+' : '';
return 'UTC ${prefix}${diff} 小时';
}
这里要特别注意:直接用 DateTime.now() 和 TZDateTime.now() 做 difference 会因为时区基准不同导致误差。正确做法是先把两者都转成 UTC 毫秒时间戳再求差:
dart复制final localUtc = local.toUtc().millisecondsSinceEpoch;
final remoteUtc = remote.toUtc().millisecondsSinceEpoch;
final diffHours = (remoteUtc - localUtc) ~/ (1000 * 60 * 60);
7.2 模拟时钟的“城市标签”设计
模拟时钟不仅要显示指针,还得告诉用户这是哪个城市的时间。我在表盘下方放了城市名,表盘背景用渐变色区分白天和黑夜——比如城市处于白天时表盘是浅色,夜晚是深色。这个效果虽然简单,但用户反馈特别好,认知负担低。
7.3 主题适配与动态切换
OpenHarmony 设备上做深色模式适配需要注意:系统主题切换时,Flutter 的 Theme.of(context) 变化了,但 CustomPainter 里的颜色是写死的。我的做法是:把所有颜色集中到一个 ClockPalette 类,根据主题模式构建调色板,CustomPainter 构造函数里传入调色板参数,这样重绘时自然用到新配色。
8. 常见问题与排查技巧实录
8.1 问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 真机运行秒针卡顿 | 使用了 Timer 而非 Ticker | 改用 Ticker,让刷新与帧率同步 |
| 城市时间差 1 小时 | 直接用固定 utc_offset 未处理夏令时 | 改用 tz 数据 + IANA 时区名 |
| 打包失败:undefined symbol | Flutter 版本与 OpenHarmony SDK 不匹配 | 核对 ohos 分支与 SDK API 版本对应关系 |
| 搜索框键盘遮挡结果 | Scaffold resizeToAvoidBottomInset 默认行为 | 设置 false + SafeArea + Expanded 列表 |
| 数字时钟秒位跳动宽度变化 | 使用了非等宽字体 | 使用 Roboto Mono 或等宽数字字体 |
| hap 包安装失败 | 包名冲突或签名问题 | 卸载旧包,检查模块名和证书配置 |
| 后台恢复时间不刷新 | 缺少 resumed 生命周期监听 | WidgetsBindingObserver 监听生命周期 |
8.2 一个隐蔽的时间 bug:DateTime 初始化
我在开发中遇到过一个很隐蔽的 bug:用 DateTime(2024, 1, 1) 这种构造函数创建时间对象时,它使用的是设备本地时区。如果你用这个时间去对比 UTC 时间戳,结果会差 8 小时(东八区)。统一原则:所有功能内部计算都用 UTC 毫秒时间戳,只在展示时才转换为具体城市本地时间。
8.3 用 hilog 调试 Flutter on OpenHarmony
真机联调时,我常用的命令:
bash复制hdc shell hilog | grep Flutter
这个命令能实时看到 Flutter 的 debugPrint 输出。如果你用了 debugPrint 但日志没打出来,检查一下 flutter run 有没有绑定成功。我用 DevEco Studio 调试时,偶尔会出现 attach 失败,重启 hdc 服务即可:
bash复制hdc kill
hdc start
8.4 真机性能监控
没有 Perfetto 的情况下,用 hdc 命令行监控 CPU 和内存占用:
bash复制hdc shell top -n 1 | grep worldclock
如果发现 CPU 持续 30% 以上,说明有严重的持续刷新问题。用 hilog 看有没有频繁的 Vsync 丢帧警告。优化手段就是从全局 setState 改为局部刷新,以及给 CustomPainter 加缓存层。
9. 实测效果与后续扩展方向
整个项目从设计到跑通,在 RK3588 开发板和几台 OpenHarmony 手机上做过真机验证。模拟时钟的扫秒效果在 60Hz 刷新率下非常平滑,CPU 占用大约 8%,内存稳定在 120MB 左右,HAP 包体积约 35MB(含 Flutter 引擎)。城市搜索响应在本地数据量几百条的情况下,基本是即时返回,300ms 防抖感知不到延迟。
这套架构后续扩展空间很大。我已经在规划几个方向:
- 闹钟与定时器:时间同步引擎和状态管理已经就绪,加闹钟功能只需要接系统通知。
- 世界时间地图:基于经纬度数据,叠加自定义地图,点击地图任意位置显示当地城市和时间。
- 更多表盘主题:CustomPainter 的逻辑决定了表盘可以做得更多样,后续会跟进劳力士风格、简约线条、翻页钟(Flip Clock)等样式,全部走自定义绘制方案。
最后再分享一个小技巧:做这类高保真时钟 App,调试阶段一定要把模拟器的系统时间调成不同时区和夏令时切换日,验证时间计算是否正确。这个我在排查夏令时 bug 时帮了大忙。如果一上来就在真机测试,本地时区没有夏令时规则,很多问题根本暴露不出来。
