JsonObject.optLong 导致的Bug

问题背景

业务场景存在一个JSB,FE同学传了个JSON到端上,端上测试的时候都是正确的,有天调试Server反馈说id值找不到,端上id值回传错了,开始排查代码。
发现Jsb传输过来的Json都是String to String的格式, 类似这样

1
2
3
4
{
"id1":"123456789087979797",
"id2":"123456789087979798"
}

断点发现,FE传过来的值是123456789087979797,但是端上取了之后,传给server的值是123456789087979792,于是排查转换过程。
端上在取id的时候,使用了org.json.JSONObject#optLong(java.lang.String) 方法,造成整数转换错误。

源码分析

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
org.json.JSONObject#optLong(java.lang.String)

public long optLong(@Nullable String name) {
return optLong(name, 0L);
}

public long optLong(@Nullable String name, long fallback) {
Object object = opt(name);
Long result = JSON.toLong(object);
return result != null ? result : fallback;
}

static Long toLong(Object value) {
if (value instanceof Long) {
return (Long) value;
} else if (value instanceof Number) {
return ((Number) value).longValue();
} else if (value instanceof String) {
try {
// 留意这里会造成bug
return (long) Double.parseDouble((String) value);
} catch (NumberFormatException ignored) {
}
}
return null;
}

原理分析

为什么Long转Double会失真,我们先复习一下Double的表示方式, 那就是IEEE754标准,
image.png
所有的浮点数都要表示成 尾数和指数的形式,这里就会有一个问题,尾数保留的精度。我们以前面的Long值123456789087979797举例,转换二进制, 总共有57位

1
110110110100110110100101110101010101100110000100100010101

换成标准形式,尾数只能存52位,后面的得丢掉,整数就在这里失真了

1
1.1011011010011011010010111010101010110011000010010001|0101(丢掉)

再转换成Long值就是 123456789087979792
源码中,并不是double转long造成的精度丢失,而是double表示long的时候,尾数精度丢失

1
return (long) Double.parseDouble((String) value);

double的尾数精度只有52位,2^(52)< 16个9,所以十进制下,double能准确表示小数的精度在15位,还有个经问题0.1+0.2 = 0.30000…4也是尾数精度导致的。本质上计算机存储的数都是离散的,不是连续的。

总结

每次使用 org.json.JSONObject#optLong的时候,留意一下目标值的类型,你可能恰好取对了值,但不总是对的,一旦值过大的case, 就会触发bug。