【Android】對JSONObject拋ConcurrentModificationException的一點思考

對JSONObject拋ConcurrentModificationException的一點思考

問題背景

NetUtil.get(url, arg, new NetUtil.Callback() {
    @WorkerThread
    @Override
    public void onSucceed(JSONObject repose) {
    }

    @WorkerThread
    @Override
    public void onFailed(String msg, Throwable t) {
        try {

            JSONObject event = new JSONObject();
            event.put("event_name", "request_error");
            event.put("error_msg", String.valueOf(msg));
            event.put("stacktrace", null == t ? "" : Log.getStackTraceString(t));

            // 上報請求失敗
            statics(event);
            // log顯示失敗詳情,
            // 此處拋ConcurrentModificationException
            Log.e(TAG, "request product list error: " + event);

        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
});

// 打點上報
private void statics(final JSONObject event) {
    ThreadUtil.bi().submit(new Runnable() {
        @Override
        public void run() {
            // 添加公共參數
            event.put("__client_ms", System.currentTimeMillis());
            event.put("__client_ver", BuildConfig.VERSION_CODE);
            // 上報
            report(event);
        }
    });
}

這看起來只有局部變量,好像沒什麼問題…

問題原因

ConcurrentModificationException是一個併發讀寫的問題,發生在不同線程的讀寫一個線程不安全的集合時。

以上代碼,看似沒有問題,而且也沒看到有顯式的線程不安全的集合。

其實不安全的集合是有的 JSONObject:在Log.e(TAG, "request product list error: " + event);中,隱藏了一個toString的語法糖: event.toString();跟進去以後是這樣的:

public class JSONObject {
    /**
    * Encodes this object as a compact JSON string, such as:
    * <pre>{"query":"Pizza","locations":[94043,90210]}</pre>
    */
    @Override public String toString() {
        try {
            JSONStringer stringer = new JSONStringer();
            writeTo(stringer);
            return stringer.toString();
        } catch (JSONException e) {
            return null;
        }
    }

    void writeTo(JSONStringer stringer) throws JSONException {
        stringer.object();
        for (Map.Entry<String, Object> entry : nameValuePairs.entrySet()) {
            stringer.key(entry.getKey()).value(entry.getValue());
        }
        stringer.endObject();
    }
}

可以看到,這裏有個nameValuePairs的全局變量,它的聲明是:

public class JSONObject {
    private final LinkedHashMap<String, Object> nameValuePairs;
}

再來看,背景中的兩行代碼:

// 根據上面的分析,這裏有個添加公共參數的寫操作。
statics(event);
// 根據上面的分析,這裏是一個讀操作。
Log.e(TAG, "request product list error: " + event);

到這裏就清晰了,
讀寫不再同一個線程,又存在對象作用域拓展的問題,自然有概率發生Crash。

問題解決

// 打點上報
private void statics(JSONObject event) {
    // 重新創建一個對象
    final eventObject =  new JSONObject(event.toString());
    ThreadUtil.bi().submit(new Runnable() {
        @Override
        public void run() {
            // 添加公共參數
            eventObject.put("__client_ms", System.currentTimeMillis());
            eventObject.put("__client_ver", BuildConfig.VERSION_CODE);
            // 上報
            report(eventObject);
        }
    });
}

問題覆盤

上面的問題,說複雜其實不復雜,一般有一些多線程相關的基本功都可以理解,但確實有些隱祕。

上報是一個很常見的案例,經過抽象抽象,代碼大同小異,模型線程模型基本可如上述所示。這裏想要反思的並不是埋點,亦或是解Crash,併發的問題一般不是必現問題,多半靠腦補。這裏值得一提的,是迪米特法則 和 耦合程度。(可參考:https://blog.csdn.net/Fantastic_/article/details/84501992

很多人喜歡利用引用傳參,然後內部修改屬性,進而達到值返回的效果。其實仔細想想,這樣拓寬了對象的作用域,延長了生命週期。不論是從軟件耦合的角度,或者是設計模式的角度,其實這是有欠妥當的做法。或許筆者這麼說會引來很多不屑(比如RxJava?)。

蘿蔔白菜各有所愛,這些古老的東西,看起來是有些過時,但不得不說,細細品味其中不乏有大智大慧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章