这两者都是获取时间戳的重要方法,但它们的设计目的、特性和适用场景有根本性的不同。用错它们会导致应用出现极其诡异且难以调试的bug。
核心结论
SystemClock.elapsedRealtime()
: 用于测量时间间隔。它是一个单调递增的时钟,只关心过了多久,不关心"现在几点"。最适合用于计时、性能监控、超时判断等。System.currentTimeMillis()
: 用于获取日历时间(Wall Time)。它表示从UTC时间1970年1月1日午夜开始的毫秒数。它会因为用户修改时间、网络自动同步、时区切换等原因而发生跳跃。最适合用于记录事件发生的具体日期和时间。
详细对比分析
特性 | SystemClock.elapsedRealtime() |
System.currentTimeMillis() |
---|---|---|
时间基准 | 从系统启动(包括深度睡眠)开始计算的时间。 | 从 Unix 纪元(1970-01-01 00:00:00 UTC) 开始计算的时间。 |
单调性 | 是单调时钟。时间只会向前走,不会因为系统时间被修改而回退或跳跃。 | 非单调时钟 。时间可能被用户、网络时间协议(NTP)或时区变化而向前或向后修改。 |
休眠影响 | 包括设备深度睡眠(休眠)的时间。即使屏幕关闭,CPU休眠,这个值也在持续增加。 | 不包括休眠时间。当设备休眠时,这个时间实际上是"冻结"的。 |
主要用途 | 测量时间间隔、耗时、性能分析、超时检测、闹钟等需要稳定时间基准的场景。 | 记录事件发生的具体日期和时间,如日志时间戳、消息发送时间、日历事件等。 |
可变性 | 不可变。它的增长只与物理时间的流逝有关,用户无法改变。 | 可变。用户可以在系统设置中轻松修改这个时间,系统也会自动根据网络进行校准。 |
场景化示例
1. 计时器或秒表功能
假设你要做一个跑步App,需要记录用户跑步用了多久。
-
错误做法:
javalong startTime = System.currentTimeMillis(); // 开始跑步 // ... 用户跑步中 ... long elapsedTime = System.currentTimeMillis() - startTime; // 计算耗时
风险 :如果用户在跑步过程中接到了电话,网络同步了时间,或者他自己修改了系统时间,你的
elapsedTime
会变得完全不准,可能突然变成负数或一个巨大的值。 -
正确做法:
javalong startTime = SystemClock.elapsedRealtime(); // 开始跑步 // ... 用户跑步中,即使设备休眠也没关系 ... long elapsedTime = SystemClock.elapsedRealtime() - startTime; // 计算真实耗时
优点 :无论系统时间如何变化,设备是否休眠,
elapsedTime
都能准确反映用户实际跑步花费的时间。
2. 记录消息发送时间
你要在聊天App中显示每条消息发送的准确时间。
-
正确做法:
javalong timestamp = System.currentTimeMillis(); // 消息发送的时刻 // 将 timestamp 保存到数据库,并发送给服务器 // 在UI上显示时,需要转换成日期字符串:new Date(timestamp).toString() 或使用 SimpleDateFormat
原因:你需要的是这个消息在"真实世界"中发生的时间点,而不是一个相对的时间间隔。即使用户后来修改了时间,这条消息记录的是"当时"的时间。
-
错误做法:
javalong timestamp = SystemClock.elapsedRealtime(); // 千万不要这样做!
后果:你得到的是一个从手机开机以来的相对时间,完全无法还原成可读的日期和时间(除非你同时记录了开机时刻的绝对时间,但这非常复杂且不必要)。
进阶细节与最佳实践
-
SystemClock.uptimeMillis()
: 这也是一个单调时钟,但它不包括设备深度睡眠的时间 。它常用于需要CPU持续运行的场景,例如Handler的延时任务 (postDelayed
)。系统会保证在uptimeMillis()
到达指定时间前,即使设备休眠也会唤醒CPU来执行任务。而elapsedRealtime()
是包括休眠时间的。 -
AlarmManager 的设置:
- 设置闹钟时,使用
ELAPSED_REALTIME
或ELAPSED_REALTIME_WAKEUP
作为闹钟类型,可以保证闹钟在精确的时间间隔后触发,不受系统时间修改的影响。 - 使用
RTC
或RTC_WAKEUP
作为闹钟类型,则会在一个特定的日历时间点触发,例如提醒你在明天上午9点开会。
- 设置闹钟时,使用
-
性能监控 :在代码块前后使用
elapsedRealtime()
来测量执行耗时是标准做法,因为它稳定可靠。javalong start = SystemClock.elapsedRealtime(); // ... 执行一些操作 ... long cost = SystemClock.elapsedRealtime() - start; Log.d("Performance", "操作耗时: " + cost + " ms");
-
混合使用:在某些复杂场景下,你可能需要混合使用两者。例如,你想实现一个"在明天早上8点提醒我"的功能,但需要保证即使用户修改了时间,提醒也能准确触发。一个常见的策略是:
- 用
System.currentTimeMillis()
计算出明天早上8点的绝对时间戳。 - 用
当前 elapsedRealtime() + (明天8点的时间戳 - 当前 currentTimeMillis())
来计算出相对于单调时钟的延迟时间。 - 使用
AlarmManager.set(ELAPSED_REALTIME_WAKEUP, calculatedDelay, pendingIntent)
来设置闹钟。
- 用
总结
如果你的需求是... | 那么你应该使用... |
---|---|
测量一段代码的执行时间、给操作计时、实现秒表 | SystemClock.elapsedRealtime() |
设置一个相对于现在的延时(如下载超时15秒) | SystemClock.elapsedRealtime() |
记录事件发生的具体日期和时间(用于显示) | System.currentTimeMillis() |
设置一个在特定日历时间点发生的提醒 | System.currentTimeMillis() (通常与AlarmManager的RTC类型配合) |
Handler的postDelayed 或需要CPU唤醒的定时任务 |
SystemClock.uptimeMillis() (系统内部使用) |
牢记这条黄金法则:问"过了多久?"用 elapsedRealtime
;问"现在几点?"用 currentTimeMillis
。 遵循这个原则可以避免绝大多数时间相关的错误。