老谭笔记

NSJSONSerialization和NSNumber给我们挖的一个大坑

最近和同事在开发过程中遇上一个很诡异的BUG,大致路径是这样的,从服务器返回的JSON字符串中获得一个NSNumber对象(保存着uint64_t值),然后通过SQLite保存在本地(此处采用了便捷的FMDB库),当再次从SQLite中读出数据时,却发现值发生了改变.

通过调试,最终在FMDB中以下这段代码中定位到了问题:

1
2
3
4
else if (strcmp([obj objCType], @encode(double)) == 0)
{
sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
}

纳尼??为何会进入这个条件分枝呢?这段代码相信并不陌生,经常在各种库中都能看到,用来根据NSNumber中的保存的值类型来读取相应值,可我们的实际是unsigned long long啊.

通过输出obj的类型,我们发现它是NSDecimalNumber,而NSDecimalNumber的objCType为d,也就是double类型.那么这就已经进入了第一个坑了, NSDecimalNumber本是用于保存精度更高的十进制数据,当然包括了double和uint64_t的范围.而objcType简单认为是double是否有些不妥?

那么问题来了,我的uint64_t值是什么时候给变成NSDecimalNumber了呢,通过以下这段测试就能得出结论了:

1
2
3
4
5
6
7
8
9
NSNumber *number = @(UINT64_MAX);
NSLog(@"%@ type:%s",[number className],[number objCType]);
NSDictionary *info = @{@"key":number};
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:info options:0 error:NULL];
info = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL];
number = info[@"key"];
NSLog(@"%@ type:%s",[number className],[number objCType]);

可以看到如下的输出:

1
2
__NSCFNumber type:Q
NSDecimalNumber type:d

所以这就是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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (NSNumber *)fixNumber:(NSNumber *)object
{
if ([object isKindOfClass:[NSDecimalNumber class]])
{
NSDecimal decimal = [(NSDecimalNumber*)object decimalValue];
// 只处理没有小数位的数
if (decimal._exponent >= 0)
{
// 有效数
uint64_t realvalue = 0;
for (int i=0; i<decimal._length; i++)
{
realvalue += (uint64_t)decimal._mantissa[i]<<(sizeof(unsigned short)*8*i);
}
// 乘以幂
if (decimal._exponent > 0)
{
realvalue *= (uint64_t)pow(10, decimal._exponent);
}
// 正负符号
if (decimal._isNegative)
{
object = [NSNumber numberWithLongLong:-realvalue];
}
else
{
object = [NSNumber numberWithUnsignedLongLong:realvalue];
}
}
}
return object;
}

当然这也并非完美的解决办法,因为NSDecimalNumber的范围大于uint64_t所能表示的范围,但既然为整型,高位溢出的处理比精度损失肯定更符合计算机的通用做法.