问题现象
使用long long 格式保持服务端返回的时间戳,然后本地展示该时间戳时,发现总是差一两分钟
NSDate *currentDate = [NSDate date];
//模拟获得服务器传回的单位是毫秒的时间戳
long long timeInterval = [currentDate timeIntervalSince1970] * 1000;
//毫秒转化为秒,再转化为日期类型
NSDate *convertDate = [NSDate dateWithTimeIntervalSince1970:(timeInterval/1000.f)];
NSLog(@"currentDate:%@,convertDate:%@",currentDate,convertDate);
输出效果: 这里注意分钟和秒已经出现了误差(实际是精度丢失)
currentDate:Wed Jun 10 11:17:34 2020,
convertDate:Wed Jun 10 11:18:24 2020
问题原因
时间戳(精确到毫秒)使用long long 格式存储,2020年目前的时间戳位数是13位,long long绝对足够存储下来,由于是整数类型,精度也肯定不会丢失。
但是当将该时间戳转化为NSDate类型时,精度缺丢失了,问题肯定出现在下面这行代码中:
NSDate *convertDate = [NSDate dateWithTimeIntervalSince1970:(timeInterval/1000.f)];
这行代码中,存在一个隐式类型转换,long long / float 类型,默认得到的是float类型,float类型存不下一个时间戳吗?答案是肯定能存下,但是精度没保障。
float的十进制精度是小数点后6~7位,能绝对保证的只有6位。
粗略得看一下这个13位的时间戳1591760542219,转化为浮点数的十进制1.1591760542219 * 10^13,即1.159176之后的小数都是无法精确保证。
实际测试,13位时间戳(毫秒)
1591760542219
移除1000.0得到的浮点数时间戳(单位是秒)
1591760512.000000
对比已经出现了精度丢失,丢失的范围在100秒以内,这也是上面问题现象中时间出现偏差的原因;
问题解法
- 本地使用double存储服务端给的时间戳
- 将代码中的隐式转换去掉,强制将long long 格式的时间戳转为double后再除以1000.0
根本原因:float的表示精度很低
这就要从浮点数的表示法讲起了IEEE754 浮点数的表示方法
浮点数表示法的简要总结
浮点数在计算机中的存储格式如上图所示,由符号位,指数,尾数三部分构成,float和double的区别仅体现在指数和尾数所占位数不同。
以float为例,符号位1bit,指数占8bit,尾数23bit。为了直观简化得说明,我们假设其指数和尾数都是以原码(正数符号位为0,负数符号位为1,其中指数部分也包含一个符号位)的方式来表示,简化的示例:
0.75 = 0100 0000 1100 0000 0000 0000 0000 0000
符号位 0 表示为正;指数位1000 0001表示为-1 尾数100 …… (此处省略20个0)表示1.1(二进制是1.1,十进制就是1.5)(由于尾数使用“二进制的科学计数法”,所以首位的整数部分的1默认存在,不占实际的空间,仅存储小数点后面尾数,这也是为什么取名为尾数的原因吧) 所以
0.75 = 1.5 * 2^(-1)
通过上面的示例,我们可以知道,决定float表示范围的是指数,决定float表示精度的是尾数;
再来粗略得计算一下float的表示范围:
8bit 指数,可以表示的指数为 -126~+127(0和255有其它用户,这里不展开讲)则float能表示的最大值为
floatMax = +(1.11111111111111111111111) × 2 ^127 ≈ 3.402823 e +38
这里用10进制直观的展示一下340282346638528859811704183484516925440.000000,整数位有39位
对应的float能表示的最小值为
floatMin = -(1.11111111111111111111111) × 2 ^(-126) ≈ −1.175494e −38
float能表示的精度由23bit的尾数决定,其最大值为2^23-1 = 8388607,也就是说尾数数值超过这个值,float将无法精确表示,所以float最多能表示小数点后7位,但绝对能保证的为6位(这里指科学计数法的方式),用普通十进制标识一下340282346638528859811704183484516925440.000000 后面的数值范围都是无法精确表示的。
再看一眼double的表示精度:尾数域为52位,最大值2^52−1=4,503,599,627,370,495 所以双精度浮点数的十进制的精度最高为 16 位,绝对保证的为15位,
所以float能表示的数值范围很大(+-10^38),但是对于一个精确到毫秒的只有13位有效数值的时间戳却无能为力,iOS系统存储的时间戳也是默认使用的double类型,所以自己处理时间戳格式类型转换时,需要注意float的表示精度问题。
参考文献:IEEE754 浮点数的表示方法