從原始碼分析JSONObject因版本差異導致toString格式異常問題

anAngryAnt發表於2018-08-31

丟擲問題

因為目前專案是以TCP通訊為主,使用自研協議解析工具來解析自定義的傳輸協議。

所以並沒有引入第三方JSON解析庫,目前還是依賴原生JSON解析庫進行解析。

使用以下程式碼構建一個JSONObject,並將其toString。

public static String toJson(JsEvent jsEvent) {
  JSONObject jo = new JSONObject();
  try {
    jo.put(TYPE, jsEvent.getType());
    jo.put(IDENTIFIER, jsEvent.getIdentifier());
    String method = jsEvent.getMethod();
    jo.put(METHOD, method);
    
    Map<String,Object> joParams = new HashMap<>();
    joParams.put("code", code);
    joParams.put("msg", msg);
    
    Map<String, Object> data = new HashMap<>();
    data.put("token", "authToken");
    
    joParams.put("data", data);
    
    jo.put("result", new JSONObject(joParams));
  } catch (Exception e) {
    ....
  }
  return jo.toString();
}複製程式碼

以上程式碼可以在絕大部分裝置上得到符合JSON協議標準的string

{
  "type": "Callback",
  "identifier": "1",
  "method": "getToken",
  "result": {
    "msg": "",
    "code": 0,
    "data": {
      "token": "tgmj2o8rs9n4s24psq18bu6k6y0tycpf"
    }
  }
}複製程式碼

但在sdk <= Android 4.3的手機上,卻得到以下JSON string

{
  "type": "Callback",
  "identifier": "1",
  "method": "getToken",
  "result": {
    "msg": "",
    "code": 0,
    "data": "{token=tgmj2o8rs9n4s24psq18bu6k6y0tycpf}"
  }
}複製程式碼

可以看出來

`key=data`對應的`value`值變成一個字串了,本來應該被解析成為一個JSONObject的。

因為org.json是系統庫,於是很容易就想到是sdk版本實現差異導致的。

尋找原因

  • 從程式碼和結果上分析

在有問題的手機上得到的結果中可以看出來,一直到第二層的JSONObject,都是可以正常解析的。

對應到程式碼上

jo.put("result", new JSONObject(joParams)); 複製程式碼

可以看到,

`result`對應的`value`是一個明確的JSONObject,所以toString時,將其解析為JSON string,這沒毛病。

但為什麼第三層

Map<String, Object> data = new HashMap<>();
data.put("token", "authToken");
joParams.put("data", data);複製程式碼

使用這樣的方式去構建JSONObject,在Android 4.3以上可以正常解析,但4.3以下卻不行呢?

經過以上的分析,我們可以大概把焦點鎖定在JSONObject(Map params) 這個構造方法中 和 JSONObject#toString 這兩個方法中。

  • 從JSONObject原始碼分析

簡單闡述一下JSONObject#toString的工作原理:

呼叫writeTo 方法去遍歷JSONObject中的nameValuePairs,將key->value處理成字串的形式。在處理value時有兩種情況:

  • 如果是JSONArrayJSONObject,會遞迴呼叫writeTo 方法,繼續進行處理。

  • 如果非以上兩個特殊型別,都會將其轉換成string(呼叫Object#toString 或者其他方法),並且append到結果中。

然後再來看一下表現正常的 sdk 8.0 的JSONObject(Map params)方法實現。

 public JSONObject(Map copyFrom) {
   this();
   Map<?, ?> contentsTyped = (Map<?, ?>) copyFrom;
   for (Map.Entry<?, ?> entry : contentsTyped.entrySet()) {
     String key = (String) entry.getKey();
     if (key == null) {
       throw new NullPointerException("key == null");
     }
     nameValuePairs.put(key, wrap(entry.getValue()));
   }
 }
​複製程式碼

表現異常的sdk 4.2.2的JSONObject(Map params)方法實現。

 public JSONObject(Map copyFrom) {
   this();
   Map<?, ?> contentsTyped = (Map<?, ?>) copyFrom;
   for (Map.Entry<?, ?> entry : contentsTyped.entrySet()) {
     String key = (String) entry.getKey();
     if (key == null) {
       throw new NullPointerException("key == null");
     }
     nameValuePairs.put(key, entry.getValue());
   }
 }複製程式碼

可以很明顯的看出來,sdk 8.0在遍歷map時,呼叫wrap函式對value進行了處理。對CollectionarrayMap 這幾種集合容器做了處理,使用明確的JSONObjectJSONArray來代替它們。而sdk 4.2.2中並沒有做這樣的處理。

在之前分析的JSONObject#toString工作原理的基礎上,再回到最開始的那段程式碼中,因為第三層的key-value集合是一個Map,所以,將會呼叫Map#toString方法來生成value。

這就是為什麼那段程式碼會在不同版本的平臺上表現出差異的原因啦~

解決方案

  1. 直接在上層程式碼做修改,使用明確的JSONObject代替CollectionarrayMap 這幾種集合容器。

  2. 模仿sdk 8.0的處理方式,過載JSONObject(Map params) 這個構造方法。


相關文章