当前位置: 首页 > 科技观察

使用JSONObject时需要注意避免一个问题

时间:2023-03-20 11:37:22 科技观察

包谢豪,小米MIUI部,MIUI基础应用组通讯录开发总监。问题是在Android业务同步的逻辑代码中,使用了JSONObject来解析服务端的JSON数据。同时,由于业务的新需求,在本地数据库中使用JSONObject来缓存包括水位在内的同步相关信息,其中水位值是Long类型。但是最近发现在同步过程中下次同步时,传递给服务器的水位并不是上次服务器返回的新水位,而是有点不同。以301028292893495297L为例,服务端返回这个水位后,客户端下次上传的水位为301028292893495296L,差值为-1。问题排查通过反复检查代码逻辑,发现水位从服务端返回到下一次请求之间只发生了如下转换:仔细阅读代码不难发现,当Long水位value存储在JSON对象中,转换为String类型,读取时将其视为Long类型。那么会不会出现精度不够的问题呢?马上参考JSONObject的文档:仔细阅读代码不难发现,将Long类型的水位值存储到JSON对象中时,会转换成String类型,作为一个长型处理。因此,就会出现精度不够的问题。请参考下面的JSONObject文档:可以看出,在读取一个JSON对象的值时,如果是String类型,读取时会认为是Long类型,读取的是String类型。它是用Double解析的,所以当值超过2^52时会出现精度不足的问题。这样,遇到的问题就可以解释了。下面是Double的存储格式说明:其中Double和Long的精度测试代码很简单(入参可以提供超过2^52的long值如301028292893495297L,返回值不会为0):即在读取JSON对象的某个值时,如果原本是String类型,会读取为Long类型,通过Double解析String类型,所以当值超过2^52时,就会有精度损失的问题。另外,JSON对象中的值是Long还是String其实也比较容易被忽略。如果JSON对象用String表示,引号对应的值为String类型,否则为其他类型。看下面两个测试用例就一目了然:Double和Long的精度测试代码很简单(输入参数可以提供超过2^52的long值如301028292893495297L):知道问题根源,修复一目了然,水位保存在JSONObject对象中时,应该保存为Long类型,而不是String类型;或者读取的时候也应该看成String类型,然后通过Long.valueOf等接口解析。另外,JSON对象中的值是Long还是String其实也比较容易被忽略。如果JSON对象用String表示,引号对应的值为String类型。看下面的试用例子一目了然:随便在网上搜索类似的问题,其实很多人都遇到过坑,比如这个。因此,虽然不能说这个库的设计是失败的,但它肯定不是一个设计良好的库。因为不能直接从API名称看出底层逻辑,容易导致用户使用不当。所以,吸取的教训是:在使用第三方库时,只要能看API文档就看API文档,千万不要照字面意思。当然,这个问题也可能仅限于Android中较旧的代码模块。毕竟新的代码会使用GSON等库来进行JSON对象操作,所以不会轻易出现这种难以发现的问题。当然,单看这个问题,其实是在添加业务逻辑的时候,没有正确使用JSONObject对象的接口。Long类型的值不应该保存为String类型,读取为Long类型。如果保存和读取的界面保持对应,就没有问题。反正这道题的教训就是在使用JSONObject相关接口的时候要格外小心。备注:Github上最新的JSON-Java库没有这个问题,可以放心使用。解决问题一旦知道了问题的根源,解决方法就会一目了然。水位存储在JSON对象中时,应保存为Long类型,而不是String类型;.valueOf和其他用于解析的接口。题完后上网搜了下类似的题。其实很多人都遇到过坑,比如这个。因此,虽然不能说这个库的设计是失败的,但它肯定不是一个设计良好的库。因为不能直接从API名称看出底层逻辑,导致使用不当。所以,吸取的教训是:在使用第三方库时,只要能看API文档就看API文档,千万不要照字面意思。当然Github上最新的JSON-Java库没有这个问题。【本文为“小米开放平台”专栏原创文章,“小米开放平台”微信公众号:xiaomideveloper]