使用流式GSON结合ByteArrayOutputStream生成JSON字符串时遇OutOfMemoryError
看来你遇到了个头疼的问题——原本想修复OOM崩溃,结果改完之后问题反而更频繁,还没法本地复现,确实棘手。咱们先拆解下当前代码的问题,再给你几个进阶优化方案:
问题根源分析
你当前的代码用ByteArrayOutputStream在内存中缓存整个JSON数组的字节数据,最后转成String的时候,还要额外分配一份和缓冲区大小一致的内存(因为String是不可变的)。当BlaModel列表很大时,这个内存开销会急剧膨胀,在内存紧张的设备上直接触发OOM。而你之前尝试的StringBuilder方案,本质也是在内存中攒整个字符串,内存占用只会更高。
进阶优化方案
1. 直接写入文件(推荐,彻底规避内存缓冲)
如果你的最终目的是保存数据,SharedPreferences并不适合存储大体积JSON——它设计来存小配置项的。更合理的做法是直接把JSON序列化到文件,完全跳过内存缓冲区:
import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import com.google.gson.Gson; import com.google.gson.stream.JsonWriter; // 全局复用Gson实例(Gson是线程安全的) private static final Gson GSON = new Gson(); private static boolean saveBlaModelsToFile(List<BlaModel> blaModels, File targetFile) { try (FileOutputStream fos = new FileOutputStream(targetFile); OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); JsonWriter writer = new JsonWriter(osw)) { writer.setIndent(" "); writer.beginArray(); for (BlaModel model : blaModels) { // 直接把单个模型序列化到文件流,内存只保留当前模型的临时数据 GSON.toJson(model, BlaModel.class, writer); } writer.endArray(); return true; } catch (IOException e) { // 替换成你项目的日志工具,比如Logcat或者Crashlytics e.printStackTrace(); return false; } }
优势:内存占用极低,只会在序列化单个BlaModel时占用少量内存,完全不会出现大内存块分配的情况。
2. 拆分数据到SharedPreferences(必须用SP时的最优解)
如果业务限制必须用SharedPreferences,那可以把大列表拆成单个模型存储,彻底避免一次性序列化整个列表:
import android.content.SharedPreferences; import com.google.gson.Gson; // 全局复用Gson实例 private static final Gson GSON = new Gson(); // 保存拆分后的模型 private static void saveBlaModelsSplit(List<BlaModel> blaModels, SharedPreferences prefs) { SharedPreferences.Editor editor = prefs.edit(); // 先保存列表长度 editor.putInt("bla_models_count", blaModels.size()); // 逐个序列化单个模型并存储 for (int i = 0; i < blaModels.size(); i++) { String modelJson = GSON.toJson(blaModels.get(i), BlaModel.class); editor.putString("bla_model_" + i, modelJson); } // 用apply()异步提交,避免阻塞主线程 editor.apply(); } // 读取拆分后的模型 private static List<BlaModel> loadBlaModelsSplit(SharedPreferences prefs) { int modelCount = prefs.getInt("bla_models_count", 0); List<BlaModel> models = new ArrayList<>(modelCount); for (int i = 0; i < modelCount; i++) { String modelJson = prefs.getString("bla_model_" + i, null); if (modelJson != null) { models.add(GSON.fromJson(modelJson, BlaModel.class)); } } return models; }
优势:每次只处理单个模型,内存占用可控,还能规避SharedPreferences单个条目的大小限制(部分设备对SP单条数据大小有限制)。
3. 优化内存中的序列化(适合列表较小的场景)
如果必须生成完整的JSON字符串,那可以替换ByteArrayOutputStream为CharArrayWriter,减少字节到字符的转换开销:
import java.io.CharArrayWriter; import com.google.gson.Gson; import com.google.gson.stream.JsonWriter; // 全局复用Gson实例 private static final Gson GSON = new Gson(); @Nullable private static String streamBlaModelsIntoJsonString(List<BlaModel> blaModels) { try (CharArrayWriter charWriter = new CharArrayWriter(); JsonWriter writer = new JsonWriter(charWriter)) { writer.setIndent(" "); writer.beginArray(); for (BlaModel model : blaModels) { GSON.toJson(model, BlaModel.class, writer); } writer.endArray(); return charWriter.toString(); } catch (IOException e) { e.printStackTrace(); return null; } }
优化点:CharArrayWriter直接操作字符数组,避免了ByteArrayOutputStream转String时的字节解码操作,减少了一次内存复制和分配。
4. 通用Gson优化建议
- 复用Gson实例:不要在方法内每次都
new Gson(),Gson是线程安全的,可以全局初始化一次,减少对象创建的内存开销。 - 精简序列化配置:如果不需要HTML转义,可以用
new GsonBuilder().disableHtmlEscaping().create(),减少生成的JSON长度,间接降低内存占用。 - 用TypeAdapter替代反射:如果
BlaModel结构复杂,可以自定义TypeAdapter,避免Gson反射带来的额外内存开销(适合性能敏感场景)。
总结
优先推荐方案1(写文件)或者方案2(拆分存储),这两个方案从根源上解决了大内存块分配的问题。如果必须生成完整JSON字符串,再考虑方案3。另外一定要记得复用Gson实例,减少不必要的内存开销。
内容的提问来源于stack exchange,提问作者Egemen Hamutçu

