Unity萌新开发游戏,读取txt数据产生的GC总结

研究背景:

        因游戏属性面板大多数需要(伪)实时刷新,一次性批量读取多个txt数据储存文本,在刷新帧产生了大量的GC分配,帧数可能不稳定产生卡顿(主要问题)

        而大部分文章减少GC分配的方法,对于部分萌新很难理解,就算是按照解决方案优化代码,刷新帧产生的GC仍然保持原有高度。

        所以我进一步的从宏观上来尝试,是否有更好的、粗暴的理解方法使萌新们(我)不用在数据读取上产生纠结,并且可以简单理解GC到底是个什么东西,小型游戏怎么可以做到忽略频繁读取带来的影响?(主要目的)

        若有理论出现问题,请您留言,我看到后第一时间进行修改。

引用概念:

        GC全称是garbage collection,即垃圾回收,顾名思义就是一种释放内存垃圾的机制。这种机制主要作用在堆空间上,常在Window->Profiler的Cpu上观察情况。

        文章: 具体GC总结-知乎 或 闭包-CSDN 或 简单GC机制优化总结-CSDN

批注:本文适合萌新在创作小型,文字,剧情游戏的数据读写的疑惑,其他均不考虑在内。

开始尝试:

        ①.采取引用的,以及其他的文章所说的几点,进行优化

         1.使用对象池:简单的说,就是将一些常用预制件提前生成,并使用带有public属性List<T>变量进行储存,反复读取利用(常使用于批量单预制件的情况,例如枪械类游戏射击的子弹,亦或是大世界某种怪物的刷新。)

         2.使用StringBuilder代替 常需要重复修改的string字符串:很好理解,不做解释

         3.尽量少循环(或Update)生成临时变量:函数内的临时变量就好比一次性创可贴,打开,止血,扔掉,这种过程会产生GC分配,循环调用临时变量会不断浪费GC(只要你不一次性循环使用1W或者10W个,我个人认为对萌新来说,不算什么)

using System.Text;
     
    public StringBuilder temText = new StringBuilder();
    
    public void Example()
    {
        temText.Clear();
        // 清空    
        temText.Append("举例");
    }

        4.减少装箱操作:简单的说,装箱就是把值类型变成引用类型,拆箱就是把引用类型变成值类型。

        c#的值类型一般包括  整型(byte、sbyte、long、ulong、short、ushort、 int、uint)、浮点数类型(float、double)、十进制类型(decimal)、字符类型(char)、布尔型(bool),枚举类型,结构体。

        c#的引用类型一般包括  类(class开发者自定义类、object基类、string类)、接口(interface)、数组(array)、代理(delegate)。

        这就很好理解了吧?

    int a = 0 ;
    string b = a.ToString();

        这就是装箱(因此你会发现,大多数类型都可以转化成string或object,但string转化成int就很麻烦,需要Convert.ChangeType等进行拆箱

        5.慎重闭包:闭包是指有权访问另一个函数作用域中的变量的函数,萌新不用理解,因为很少用得上,需要具体请查看引用文章

        6.减少使用返回数组(或List<T>)的函数,大量传值函数的频率:理论上返回数组都是一份拷贝,相当于是new了一个新数组,会产生临时变量占用,传值问题有待研究。

        7.使用for代替foreach:5.5版本以前会产生装箱操作,我个人认为萌新应该会很少使用foreach,例如我。

        8.关键帧限制协程调用:startcoroutine()实际上是new一个对象,yield retrun new XXX 也一样,这个一般没事,不用在意。

        9.struct中不要有引用类型变量(struct是值类型,而如果struct中有引用类型的变量,GC会检查整个struct)【增加GC的方式是让它检查不必检查的对象】(来自引用文章)

        ②.虽然文本的重点是第二部分,但我还是希望各位萌新可以了解第一部分增强自己的代码书写规范,这对你有好处。

        切忌刻意,就因为GC消耗就换个写法,这是绝对不可以的,还是要根据实际情况!

        这九种重点固然有用,但我优化完整个代码流程后(萌新知识不够,无法发现九种外的问题了怎么办?)还是无法解决高频读取txt带来的GC。

using System.IO;
    File.ReadAllText(path, Encoding.UTF8);

        这个时候,我就“发现了”,会不会不是我的代码垃圾,而是本身调用的函数就会带来GC消耗呢?(萌新思路)

        宏观事实还真就是这样,下面这一条数据流读取,单帧达到了恐怖的7.7KB

var time = System.IO.File.ReadAllText("C:/Users/Administrator/AppData/LocalLow/a/cs.txt", Encoding.UTF8);

         这是一个多么令人惊愕的数值!它增加了单帧0.23ms的运算延迟,而习惯于批量读写txt的萌新,一款文字游戏可能会存在高达[10~50]个的数据变量循环读取,每帧会造成[100~400]的GC,额外损耗[2~10]ms的运算。

        这是一个什么概念?我说一下萌新肯定就能理解。

        宏观上,游戏1秒会运行n帧,这里我们用60帧来计算,而1秒也就是1000ms,平均每帧的占有时间也就是1000/60≈16.67ms。

        也就是说若16.67ms内,该帧没完成脚本的运算,就会影响帧率,导致肉眼可见的卡顿。

        但萌新你要知道,你写的脚本只是一部分啊,还有UnityMono基类等等脚本的引用运算。每帧额外损耗[2~10]ms是一个非常要命的问题。

        说到这里,你肯定能想出一个合理方案了吧!

        没错,重点就是通过我上面标红的高频读取四字。

        ③.解决方案

        高频很好理解,通过降低循环(或Update)运行频率,一般Update通过Time.deltaTime,循环通过协程yield return XXX进行延迟。

    private float timer = 0;
    void Update() 
    {
        //timer += Time.deltaTime;
        // if (timer >= 1f) {
        //     RefreshedStatus();
        //     timer = 0;
        // }
    }
    
    IEnumerator xc()
    {
        for(int i=0;i<xc;i++)
        {
            //yield return null;
            //yield return new WaitForEndOfFrame;
        }
    }

        等等等等,方法太多,不一一举例,明白意思即可 不过还是要注意协程 yield return new XXX的使用。

        读取,就涉及到了一个问题,数据流到底是因为文件内容,还是因为打开文件关闭文件的操作产生GC呢?

        经过我简单的测试,打开多个文件、一个文件内容不同时二者GC相差甚大。

        因此不难推测出,打开多个文件的GC消耗远超于一个文件(所有文件内容并合到一起)

        也就是说,所有的数据文件最好类似JSON的格式统一储存到一个文件里,读取使用遍历截取即可(至于函数,我建议自己按照舒服的格式去写。)

        ④.想法拓展

        以上两种效果很好,萌新操作也不会有任何问题,但肯定有其他的方法也能减少GC带来的影响、 例如:

        1.不使用ReadAllText一次性读取,而是使用读行的方式?

        2.每帧50条数据刷新修改为两帧刷新50条?

        3.非每次刷新都要创建临时变量或赋值,而是先判断当前与上条是否有差异,有差异再赋值

        4.采用内存储存,直到游戏记录点再进行数据储存?

文章有点虎头蛇尾,希望萌新提取精华使用,若是有新的方法也可在评论区留言!下次再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值