You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动