Libgdx游戏中使用Kryo序列化对象时出现重复栈帧输出问题的原因排查
解决Kryo序列化Libgdx复杂对象图时的栈溢出问题
兄弟,我之前做Libgdx游戏存档时也碰到过一模一样的坑——调大栈空间到几百兆都没用,控制台全是重复的栈帧,连明确的异常都看不到。其实这不是单纯的栈大小不够,而是Kryo递归序列化复杂对象图时,调用栈深度超过了VM的承受极限,哪怕给300M栈空间,对象层级要是有几千层,照样会溢出。下面是我踩坑总结的解决方案,按优先级来:
1. 先定位到底是哪个对象搞的鬼
别上来就瞎改配置,先搞清楚问题根源。你可以试试这两个方法:
- 逐步排查字段:把GameDataBase里的一半字段注释掉,尝试序列化;如果没问题,再逐步加回去,很快就能找到触发栈溢出的那部分对象。
- 开Kryo调试模式:在初始化Kryo时加一行
kryo.setDebug(true);,控制台会打印每一个被序列化的对象,看最后停在哪个对象上,那大概率就是罪魁祸首。
2. 优化对象结构才是治本之道
栈溢出的核心还是对象图太复杂——要么层级太深,要么循环引用绕来绕去。我当时是这么处理的:
- 拆解深嵌套结构:比如我之前的任务系统是
GameData -> TaskManager -> TaskGroup -> Task -> SubTask的五层嵌套,直接序列化就炸了。后来改成把所有任务存在全局Map里,每个对象只存父任务的ID,序列化时只存ID,读档再重建引用,瞬间解决了递归问题。 - 清理无意义的循环引用:检查你的对象里有没有A引用B、B又引用A的情况,如果这些引用不需要序列化,直接加
transient;如果需要保留,就用ID代替直接对象引用。 - 拆分大集合:如果某个ArrayList里的对象本身带一堆嵌套集合,别一次性序列化整个集合,拆成几个小模块分开处理。
3. 给Kryo做针对性配置
光改结构不够,还要调整Kryo的行为适配你的对象:
- 提前注册所有要序列化的类:Kryo自动检测类时会额外增加递归开销,提前注册不仅更快,还能避免很多奇怪的问题。比如:
// 在初始化Kryo的时候添加注册 kryo.register(GameDataBase.class); kryo.register(PlayerData.class); kryo.register(ArrayList.class); // 把你用到的自定义类都注册一遍 - 给深层级对象写自定义序列化器:像链表、树这种天然递归的结构,Kryo默认的序列化器是递归式的,很容易栈溢出。你可以写一个迭代式的序列化器,比如:
然后注册这个序列化器:public class LinkedObjectSerializer extends Serializer<LinkedObject> { @Override public void write(Kryo kryo, Output output, LinkedObject obj) { LinkedObject current = obj; // 迭代遍历链表,避免递归调用 while (current != null) { kryo.writeObject(output, current.getData()); current = current.getNext(); } output.writeBoolean(false); // 标记链表结束 } @Override public LinkedObject read(Kryo kryo, Input input, Class<LinkedObject> type) { LinkedObject head = null; LinkedObject current = null; while (input.readBoolean()) { LinkedObject node = new LinkedObject(); node.setData(kryo.readObject(input, Data.class)); if (head == null) head = node; else current.setNext(node); current = node; } return head; } }kryo.register(LinkedObject.class, new LinkedObjectSerializer());,这样序列化时就不会触发递归调用栈了。 - 临时应急:限制序列化深度:如果暂时没时间改结构,可以试试
kryo.setMaxDepth(2000);(数值根据你的对象层级调整),但这只是临时方案,还是要从结构上优化才靠谱。
4. 别一次性序列化整个GameDataBase
我当初就是把整个GameDataBase扔给Kryo才炸的,后来改成分块序列化,问题瞬间缓解:
public void save() throws FileNotFoundException { kryo.setReferences(true); // 先注册需要的类 kryo.register(GameDataBase.class); kryo.register(PlayerData.class); kryo.register(LevelData.class); Output output = new Output(new FileOutputStream("testfile")); // 分块写入:玩家数据、关卡数据、设置数据分开序列化 kryo.writeObject(output, gameDB.getPlayerData()); kryo.writeObject(output, gameDB.getLevelData()); kryo.writeObject(output, gameDB.getSettings()); output.close(); }
读档时按顺序读取这几块数据,再组装回GameDataBase就行。
5. 再检查一遍transient字段
虽然你已经把Texture、Pixmap标记为transient,但要注意Libgdx的一些对象(比如Sprite、BitmapFont)内部可能还有非transient的字段,这些字段可能引用了其他可序列化对象,导致链式递归。比如Sprite里的Rectangle字段是可序列化的,如果这个Rectangle又引用了其他对象,就会继续触发递归。你可以把包含这些Libgdx对象的整个字段标记为transient,读档时再重新加载这些资源。
内容的提问来源于stack exchange,提问作者bv256




