问题引出
在开发中,涉及价格金额处理,后台会返回 Number 类型的数据,打印或者经过 JSON 转 Model 后的 NSString 可能出现精度丢失的问题,如果涉及到金额的加减乘除运算问题将暴露得更为明显。这里就 iOS 数据精度处理做一个总结。
问题复现
NSNumber 转 NSString
1 | NSArray *numbers = @[ |
在这里我们发现将 NSNumber 转换成 NSString 的过程中可能会出现精度丢失。
JSON 到 Model
1 | //出现 BUG 的条件必须是两位数,且带两位小数,类型还必须是 float |
在这里我们发现将 JSON 解析成 Model 的过程中可能会出现精度丢失。
问题分析
因为浮点数在计算机中是采用 IEEE 规定的标准浮点格式,即二进制科学表示法。
在这种表示法中,一个数 S = M * 2 ^ N
。
其中 N 表示阶码,M 表示位数(有效数字位)。
例如一个 float 类型的浮点,在 32bit 位上,占 4 个字节,字节表示为
【31】N:【30 ~ 23】 M:【22~0】
- 31 位表示符号位: 0 正,1 负
- 中间 8 位是阶码位: 表示范围【-128 ~ 127】,对于 float 类型数据规定其偏移量为 127
- 后面 23 位是有效数字位: 因为科学计数法,整数位定死了是 1,所以这里记录的是小数点后面的二进制为
指数 N 决定它的范围,因为 M 总是一个以 1 开头的小数,以 float 来说即是:-2 ^ 128 ~ 2 ^ 128,即 float 能表示的数的大小的范围。
而它的精度是由位数(也就是有效的数据位)来决定的, 2 ^ 23 = 8388608,总共 7 位,表示最多能用 7 位有效数字,最多能表示到.8388708 即小数点后 7 位,由于不能完全表示全部的 7 位数,所以它的精度范围是 6 位~7 位。
同理可得 double 的精度是 2 ^ 52 = 4503599627370496, 共 16 位,所以精度为 15 ~ 16 位。
总结:float/double 类型的范围和精度的计算方式
不同机器字节序的规定
公式: S = M * 2 ^ N
二进制在内存中是以补码形式存储,负数要对其二进制绝对值按位取反再加一,正数的补码与原码形式相同
也就是说 float 和 doublel 类型数据在计算机中存储可能是不精确的。
当我们需要转换成浮点类型是数据时,最好用 double,因为 double 的精度更高,出现丢精度的概率相对是较小的。
在 iOS 中提供一个专用的类来处理浮点数据相关的运算: NSDecimalNumber
### 解决方案
使用 NSDecimalNumber
来进行浮点数处理。
我们给 NSString 添加一个分类来处理浮点运算问题
1 | ///.h |
如何使用
1 | NSArray *numbers = @[ |
问题得以解决。☕️
参考资料:
- https://developer.apple.com/documentation/foundation/nsdecimalnumber
- http://www.jianshu.com/p/4703d704c953
- https://eezytutorials.com/ios/nsdecimalnumber-by-example.php
- http://www.skyfox.org/ios-nsdecimalnumber-use.html
- https://stackoverflow.com/questions/421463/should-i-use-nsdecimalnumber-to-deal-with-money