【Android】对JSONObject抛ConcurrentModificationException的一点思考
16lz
2021-01-23
对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: * {"query":"Pizza","locations":[94043,90210]}
*/ @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?)。
萝卜白菜各有所爱,这些古老的东西,看起来是有些过时,但不得不说,细细品味其中不乏有大智大慧。
更多相关文章
- NDK C++线程中如何调用JAVA API
- android 5.0之后利用Intent传递Serializable对象存在问题
- 【摘录】Android的线程使用来更新UI----Thread、Handler、Looper
- android 多线程编程
- Android 学习系列 - 线程模型