【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?)。

萝卜白菜各有所爱,这些古老的东西,看起来是有些过时,但不得不说,细细品味其中不乏有大智大慧。

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