You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SharedPreferences读取自定义ArrayList二次恢复触发RuntimeException问题

问题分析与解决方案

我碰到过好几个类似的Gson+SharedPreferences结合使用的坑,结合你描述的「第一次stop/resume正常、第二次触发RuntimeException」的场景,大概率是以下几个细节没处理到位,咱们一个个捋:

1. 先排查Gson反序列化的泛型类型问题(高频坑)

很多人在反序列化ArrayList时会偷懒直接写gson.fromJson(json, ArrayList.class),这会让Gson无法正确推断泛型类型——第一次可能因为数据结构简单侥幸成功,但第二次数据量增加或结构变化时就会抛出类型转换异常。

正确做法是用TypeToken明确指定泛型类型:

// 错误写法:无法正确解析泛型
ArrayList<TrackPoint> savedPoints = gson.fromJson(json, ArrayList.class);

// 正确写法:通过TypeToken告知Gson具体的泛型类型
Type trackListType = new TypeToken<ArrayList<TrackPoint>>() {}.getType();
ArrayList<TrackPoint> savedPoints = gson.fromJson(json, trackListType);

2. 检查SharedPreferences的写入提交逻辑

如果你的stop()方法用了apply()异步写入,第二次操作时可能数据还没完全写入磁盘,导致resume()读取到不完整或旧的JSON串,进而触发异常。

优化方案:

  • 在需要确保数据写入完成的场景(比如运动记录保存),改用commit()同步提交(它会返回布尔值告知写入是否成功);
  • 序列化前先拷贝一份当前运动数据,避免多线程环境下原列表被修改导致JSON生成异常:
public void stop() {
    try {
        // 先拷贝数据,避免序列化过程中原列表被修改
        ArrayList<TrackPoint> copyPoints = new ArrayList<>(mTrackPoints);
        Gson gson = new Gson();
        String json = gson.toJson(copyPoints);
        
        SharedPreferences.Editor editor = mSharedPrefs.edit();
        editor.putString("track_data", json);
        // 用commit确保写入完成,同时判断是否成功
        boolean saveSuccess = editor.commit();
        if (!saveSuccess) {
            Log.e("TrackerService", "Failed to save track data to SharedPreferences");
        }
    } catch (Exception e) {
        Log.e("TrackerService", "Error during save", e);
    }
}

3. 避免ArrayList的数据污染

如果resume()时没有先清空现有列表,直接把读取到的数据addAll进去,会导致第二次恢复时数据叠加,甚至出现循环引用或无效数据,序列化时触发异常。

正确的恢复逻辑:

public void resume() {
    try {
        // 先清空当前列表,避免数据叠加
        mTrackPoints.clear();
        
        String json = mSharedPrefs.getString("track_data", null);
        if (json != null && !json.isEmpty()) {
            Type trackListType = new TypeToken<ArrayList<TrackPoint>>() {}.getType();
            ArrayList<TrackPoint> savedPoints = gson.fromJson(json, trackListType);
            mTrackPoints.addAll(savedPoints);
        }
    } catch (Exception e) {
        Log.e("TrackerService", "Error during resume", e);
    }
}

另外,在stop()完成后,建议也清空mTrackPoints,避免下次启动时携带旧数据。

4. 检查TrackPoint实体类的序列化兼容性

如果你的TrackPoint类缺少无参构造方法,或者包含无法被Gson序列化的字段(比如自定义对象、循环引用),也会导致第二次序列化/反序列化失败。

确保实体类满足以下要求:

  • 必须有无参构造方法(Gson需要它来实例化对象);
  • 所有需要序列化的字段要么是public,要么提供getter/setter;
  • 避免循环引用(比如TrackPoint中引用了包含它的TrackerService实例)。

示例规范的实体类:

public class TrackPoint {
    private double latitude;
    private double longitude;
    private long timestamp;

    // 必须保留无参构造
    public TrackPoint() {}

    public TrackPoint(double latitude, double longitude, long timestamp) {
        this.latitude = latitude;
        this.longitude = longitude;
        this.timestamp = timestamp;
    }

    // 可选:提供getter/setter,提升代码规范性
    public double getLatitude() { return latitude; }
    public void setLatitude(double latitude) { this.latitude = latitude; }
    // ... 其他字段的getter/setter
}

5. 加日志排查精准定位问题

如果以上方案都没解决,建议在stop()和resume()中添加详细日志,打印JSON串内容、异常栈信息,这样能快速定位是写入时生成了无效JSON,还是读取时反序列化失败:

// stop()中添加日志
Log.d("TrackerService", "Saved JSON length: " + json.length() + ", content snippet: " + json.substring(0, Math.min(100, json.length())));

// resume()中添加日志
Log.d("TrackerService", "Loaded JSON: " + (json == null ? "null" : json.substring(0, Math.min(100, json.length()))));

内容的提问来源于stack exchange,提问作者user86776

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 09:27:29