Unity3D游戏GC优化总结

GC优化概述

        1、C#变量分为两种类型:值类型和引用类型,值类型分配在栈区,引用类型分配在堆区,GC关注引用类型

  2、GC卡顿原因:堆内存垃圾回收,向系统申请新的堆内存

  3、GC触发条件:堆内存分配而当内存不足时、按频率自动触发、手动强行触发(一般用在场景切换)

  4、GC负面效果:内存碎片(导致内存变大,GC触发更加频繁)、游戏顿卡

  5、GC优化方向:减少GC次数、降低单次GC运行时间、场景切换时主动GC

  6、GC优化策略:减少对内存分配次数和引用次数、降低堆内存分配和回收频率

  7、善用缓存:对有堆内存分配的函数,缓存其调用结果,不要反复去调用

  8、清除列表:而不要每次都去new一个新的列表

  9、用对象池:必用

  10、慎用串拼接:缓存、Text组件拆分、使用StringBuild、Debug.Log接口封装(打Conditional标签)

  11、警惕Unity函数调用:GameObject.name、GameObject.tag、FindObjectsOfType<T>()等众多函数都有堆内存分配,实测为准

  12、避免装箱:慎用object形参、多用泛型版本(如List<T>)等,这里的细节问题很多,实测为准

  13、警惕协程:StartCoroutine有GC、yield return带返回值有GC、yield return new xxx有GC(最好自己做一套协程管理)

  14、foreach:unity5.5之前版本有GC,使用for循环或者获取迭代器

  15、减少引用:建立管理类统一管理,使用ID作为访问token

  16、慎用LINQ:这东西最好不用,GC很高

  17、结构体数组:如果结构体中含有引用类型变量,对结构体数组进行拆分,避免GC时遍历所有结构体成员

  18、在游戏空闲(如场景切换时)强制执行GC

Protobuf-net GC优化方案

        

protobuf-net在本次协议测试中GC产生的原因总结如下:

  1、反射

  2、forearch

  3、装箱

  4、创建新的pb对象

  5、创建新的字节数组

1.去反射

        用过lua的人都知道,不管是tolua还是xlua,去反射的方式是生成wrap文件,这里去反射可以借鉴同样的思想。

        

using CustomDataStruct;
using ProtoBuf.Serializers;

namespace battle
{
    public sealed class NtfBattleFrameDataDecorator : ICustomProtoSerializer
    {
        public void SetValue(object target, object value, int fieldNumber)
        {
            ntf_battle_frame_data data = target as ntf_battle_frame_data;
            if (data == null)
            {
                return;
            }

            switch (fieldNumber)
            {
                case 1:
                    data.time = ValueObject.Value<int>(value);
                    break;
                case 3:
                    data.slot_list.Add((ntf_battle_frame_data.one_slot)value);
                    break;
                case 5:
                    data.server_from_slot = ValueObject.Value<int>(value);
                    break;
                case 6:
                    data.server_to_slot = ValueObject.Value<int>(value);
                    break;
                case 7:
                    data.server_curr_frame = ValueObject.Value<int>(value);
                    break;
                case 8:
                    data.is_check_frame = ValueObject.Value<int>(value);
                    break;
                default:
                    break;
            }
        }

        public object GetValue(object target, int fieldNumber)
        {
            ntf_battle_frame_data data = target as ntf_battle_frame_data;
            if (data == null)
            {
                return null;
            }

            switch (fieldNumber)
            {
                case 1:
                    return ValueObject.Get(data.time);
                case 3:
                    return data.slot_list;
                case 5:
                    return ValueObject.Get(data.server_from_slot);
                case 6:
                    return ValueObject.Get(data.server_to_slot);
                case 7:
                    return ValueObject.Get(data.server_curr_frame);
            }

            return null;
        }
    }
}

        反射产生的地方在protobuf-net的装饰类中,具体是PropertyDecorator,我这里并没有去写工具自动生成Wrap文件,而是对指定的协议进行了Hook。

2.forearch

        foreach对列表来说改写遍历方式就好了,我这里没有对它进行优化,因为Unity5.5以后版本这个问题就不存在了。篇首优化后的效果图中还有一点残留就是因为这里捣鬼。

3.无GC装箱

        要消除这里的装箱操作,需要重构代码,而protobuf-net内部大量使用了object进行参数传递,这使得用泛型编程来消除GC变得不太现实。我这里是自己实现了一个无GC版本的装箱拆箱类ValueObject,使用方式十分简单,类似:

        

public object Read(object value, ProtoReader source)
        {
            Helpers.DebugAssert(value == null); // since replaces
            return ValueObject.Get(source.ReadInt32());
        }
        public void Write(object value, ProtoWriter dest)
        {
            ProtoWriter.WriteInt32(ValueObject.Value<int>(value), dest);
        }

其中ValueObject.Get是装箱,而ValueObject.Value<T>是拆箱,装箱和拆箱的步骤必须一一对应。

4.使用对象池

        对于protobuf-net反序列化的时候会创建pb对象这一点,最合理的方式是使用对象池,Hook住protobuf-net创建对象的地方,从对象池中取对象,而不是新建对象,用完以后再执行回收。池接口如下:

        

/// <summary>
/// 说明:proto网络数据缓存池需要实现的接口
///
/// @by wsh 2017-07-01
/// </summary>

public interface IProtoPool
{
    // 获取数据
    object Get();

    // 回收数据
    void Recycle(object data);

    // 清除指定数据
    void ClearData(object data);

    // 深拷贝指定数据
    object DeepCopy(object data);

    // 释放缓存池
    void Dispose();
}

5.使用字节缓存池

        对于new byte[]操作的GC优化也是一样的,只不过这里使用的缓存池是针对字节数组而非pb对象,我这里是自己实现了一套通用的字节流与字节buffer缓存池StreamBufferPool,每次需要字节buffer时从中取,用完以后放回。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值