浅谈,Unity下,数据对象的内存管理和生命周期的概念

今天听到几个大佬在讨论内存泄漏的处理方案,不是我在的项目,我也不好多插嘴,
但是不认为打补丁的方式,去找泄漏的资源和内存去卸载是个正确做法,所以分享一下我以往的内存管理的经验。

从设计出发去解决问题,才是正确的处理方式。

首先 内存泄漏,给内存泄漏一个定义:
申请了内存,但是巴拉巴拉等等操作之后,这块内存没有在使用中,之后也无法使用到,这块内存被认为是泄漏的,这块内存被占领了,
无法被重复使用了。这样的事情多来几次,那么当前程序占领的内存就越来越大。超过系统的单个进程使用上限,系统就会强制结束进程。
以前的WindowsXP 好像是1.5G??记不清了。手机上就是不同的手机不一样,表现就是闪退。

在Unity里面表现例如是

 public class NewBehaviourScript : MonoBehaviour
{
    List<int> m_list = new List<int>();

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            for (int i = 0; i < 1000000; i++)
            {
                m_list.Add(i);
            }
            Debug.Log(m_list.Count);
        }
        if (Input.GetKeyDown(KeyCode.S))
        {
            GameObject.Destroy(this.gameObject);
        }
    }
}

我新建一个Scene,创建一个GameObject,挂上上面这个脚本,然后运行起来拼命点 键盘a,
嗯,和你想想的一样,内存就涨起来了。
然后我按S销毁自己的对象,
好了,这样m_list就泄漏了,因为这个List无法被其他地方使用了,但是我又不能再去操作他了,但是内存还在(可以看Profiler的Memory)

关于AssetBundle泄漏,就比较简单明了,加载了一个资源之后没有做卸载的操作。

关于AssetBundle泄漏,我之前发过一篇文章了,说的是 计数型AssetBundle加载管理(点击跳转).,主要解决的问题是,有多个对象要使用同一个AB的资源,
那么正确的卸载时间应该是在最后一个不使用这个资源的对象释放这个资源之后。这样就能明确的管理AB的生命周期。

那么刚才提到了 生命周期 的概念,什么是生命周期,怎么样算一个生命周期。

其实顾名思义,人有生命,从出生到去见上帝。
那么内存也是一样的,new就是出生了,delete就是死亡了。

对于Unity来说MonoBehaviour里 Awake就是出生,OnDestroy就是死亡。
像是C++里面的构造和析构对应就是 出生和死亡。

所以这里说到的内存管理,变量,数据结构,AB等等也都是有生命周期的,生命周期开始把对象new出来,生命周期结束,把对象给销毁掉。
那么对于AB来说,就是加载和卸载,因为有计数型AssetBundle加载管理(点击跳转). 的文章了,就不多说了,来说说数据的生命周期。

计数型AssetBundle加载管理里有说到 加载卸载对齐,
数据方面也是一样的例如:

public class NewBehaviourScript : MonoBehaviour
{
    List<int> m_list = null;

    private void Start()
    {
        m_list = new List<int>();
    }

    private void OnDestroy()
    {
        if (m_list != null)
        {
            m_list.Clear();
            m_list = null;
        }
    }
}

这就可以认为m_list有一个完整的生命周期了,在NewBehaviourScript被创建的时候 m_list为null没有内存,
我认为它应该从这个脚本的 Start时候出生,于是new了一块内存给它,然后我认为它该在这个脚本被销毁的时候被销毁。
所以就可以说是 m_list的生命周期是从这个脚本的Start开始到这个脚本销毁。

那也会有一些工具脚本,或者单例什么的,其实意思也是一样的,例如

public class Test
{
    private List<int> m_list = null;

    /*
     * 假装这里有很多逻辑
     * void function()
     * {
     * ......****
     * }
     */

    public void Init()
    {
        m_list = new List<int>();
    }

    public void Release()
    {
        if (m_list != null)
        {
            m_list.Clear();
            m_list = null;
        }
    }
}

public class NewBehaviourScript : MonoBehaviour
{
    private void Start()
    {
        Test t = new Test();
        t.Init();
        t.Release();
        t = null;
    }
}

那么这里 Test t,对象t的生命周期 就是从new开始到被赋值为null,
Test内部的m_list的生命周期 就是 从Init调用开始到Release。
在这段代码中如果t.Release();不调用,那么生命周期就不完整了,就出生没死亡,那么就很容易造成泄漏了。
那么有人会说了,我有其他写法,例如:

public class Test
{
    public List<int> m_list = null;

    /*
     * 假装这里有很多逻辑
     * void function()
     * {
     * ......****
     * }
     */

    public void Init()
    {
        m_list = new List<int>();
    }
}

public class NewBehaviourScript : MonoBehaviour
{
    private void Start()
    {
        Test t = new Test();
        t.Init();
        t.m_list.Clear();
        t.m_list = null;
        t = null;
    }
}

那么像是这样的代码,在Test里面申请了内存,在外面做释放。
这样做可以吗?回答:可以。这样会内存泄漏嘛?回答:不会。
那么这样做对吗?回答:不对!

(其实学C++应该比较容易理解,一个指针在一个类里面new,在其他类free,那么我觉得你可能要被你的上司骂死了。)

这边要说一个重点就是生命周期在一个良好的设计里,一定是成对出现的,也就是之前说的“加载卸载对齐”
其实生命周期也是需要做 “创建销毁对齐”

哪里创建,哪里销毁,保证这个对象是由当前所在的脚本去维护去管理的,这其实也体现了 访问属性public protected private的意义。
意思就是,我的手脚是我自己的,我不能分给你,你只能看一看,或者我帮你做什么事,但是手脚不是你的,是我的,你打断了我的手脚是要赔偿的(赔偿就是内存泄漏或者崩溃或者闪退)。
比喻不太恰当,差不多这个意思。嘿嘿嘿。

那在上面说的基础上,我的对象由我自己管理,那么其他模块就是使用我的对象,我管理着对象的生命周期,卸载不卸载那就是我说了算,
那么其他地方就算引用了这个对象,也不会造成泄漏,因为如果这个对象的生命周期到了,其他地方还在使用就会报错了,空指针异常,不会造成内存泄漏。
(这边的,指的是这个对象所在的脚本,拟人一下)

所以如果发现了内存泄漏,最正确的做法,首先是设计有没有问题?
在设计正确的基础上,再去找泄漏点,再针对的修补,
而不要一发现泄漏就想着去补,会导致框架的结构越来越差,到后期就会难以维护。

差不多就这点,浅谈一下。之后有机会针对写详细的文章po出来。


程序学无止尽。
欢迎大家沟通,有啥不明确的,或者不对的,也可以和我私聊
我的QQ 334524067 神一般的狄狄

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值