最近和同事在开发过程中遇上一个很诡异的BUG,大致路径是这样的,从服务器返回的JSON字符串中获得一个NSNumber对象(保存着uint64_t值),然后通过SQLite保存在本地(此处采用了便捷的FMDB库),当再次从SQLite中读出数据时,却发现值发生了改变.
通过调试,最终在FMDB中以下这段代码中定位到了问题:
|
|
纳尼??为何会进入这个条件分枝呢?这段代码相信并不陌生,经常在各种库中都能看到,用来根据NSNumber中的保存的值类型来读取相应值,可我们的实际是unsigned long long啊.
通过输出obj的类型,我们发现它是NSDecimalNumber,而NSDecimalNumber的objCType为d,也就是double类型.那么这就已经进入了第一个坑了, NSDecimalNumber本是用于保存精度更高的十进制数据,当然包括了double和uint64_t的范围.而objcType简单认为是double是否有些不妥?
那么问题来了,我的uint64_t值是什么时候给变成NSDecimalNumber了呢,通过以下这段测试就能得出结论了:
|
|
可以看到如下的输出:
|
|
所以这就是NSJSONSerialization给我们挖的另一个坑了,当number值比较大的时候,就会自动处理为NSDecimalNumber类型,比如测试的第一行代码换为NSNumber *number = @(UINT64_MAX/2);则转换之后就仍然为__NSCFNumber,经测试其它第三方JSON解析库就不会如此”智能”了.
那么如何来解决这个问题呢?方案一就是替换掉NSJSONSerialization采用第三方JSON解析库,这样就可以回避这个坑了,但综合考虑之后,我们决定去主动适应Apple给我们挖的坑,毕竟回避问题不是真的解决问题.
NSDecimalNumber保存数据的方式为:mantissa x 10^exponent这种类型的科学计数法,mantissa存储整数部分,比如120表示为12 x 10^1,而1.2表示为12 x 10^-1,所以如果需要区分NSDecimalNumber中实际保存的整数还是小数,可以直接通过指数exponent来判定.
所以我们通过这段代码来将NSDecimalNumber转换为普通的NSNumber:
|
|
当然这也并非完美的解决办法,因为NSDecimalNumber的范围大于uint64_t所能表示的范围,但既然为整型,高位溢出的处理比精度损失肯定更符合计算机的通用做法.