【系统】Unity3D中的AssetBundle资源管理器(终)

前言

水不动了,一篇小小的资源管理器已经水了好几篇了,太让人难堪了。但是也让我知道了,沉淀没有想象的那么容易,就在以为自己思考的很全面的时候,测试下来又会冒出很多问题。

正文

问题

这次遇到的问题是在以下情况下出现的,上次我们用的异步做资源加载,但有一种情况我没有考虑到(也是我实际使用时碰到的一种情况)我们需要给一个 sprite 赋值一张图片,如果是在异步的情况下,这个资源回调还没有加载回来时,又因为刷新问题需要给这个 sprite 重新赋值一张图片,如果这时旧的图片在新的图片前面加载出来,那么最终显示的图片就是我们不想要的旧图片了。这也是异步的加载的一个缺陷,我们不能保证加载回来的顺序。

解决办法

我会在加载的时候给加载的接口一个当前需要赋值的对象的索引 id —— 这个索引 id 是唯一的。将这个 id 传到资源管理器里做一次判断,如果这个唯一索引已经对应上了一个 assetname 的话,就把之前记录下来的 assetname 所对应的 asset 卸载,然后在资源管理器里和需要加载的 asset 做一次绑定。这样的话,就能保证所加载的资源不会因为异步加载的顺序所产生一些显示错误。

更新

说明

之前在讲异步加载的时候,有一个特性我遗漏了,就是异步相当于开了一个线程池来同时加载所有的东西,这样在设备高速发展的今天,我们能高效的运用多核技术,使我们的游戏更流畅的运行。

技术更新

unity 的异步接口有个很有用的功能,就是如果你直接去获取 assetbundleCreaterequest 中的 assetbundle 属性 或者 assetbundleRequest 中的 asset 属性时,它在获取这个资源的时候,属于阻塞获取,除非 assetbundle,asset 加载回来,否则进程会一直阻塞在这里。但是我们说了,异步加载其实是线程池在跑的,所以这个阻塞不会影响到其他资源已经创建的加载。我们可以根据这个特性,来做一个同步接口,这个接口的好处就是,我们可以避免在打开 ui 界面的时候 因为资源没有加载 而实例化界面对象所产生的显示问题。这是这个特性的一种好的使用方案。

还有一种使用方案是,我们可以在界面打开的时候预加载一组 AssetbundleRequest 的对象,它是异步方法,所以当我们在界面打开的时候,它可能就已经加载了一部分了。打开的时候,我们可以直接去获取它的 asset,如果没有加载成功,它就会一直阻塞在这里,直到加载成功返回,这样做的好处是能让我们感觉界面打开的更流畅。

后语

这个资源管理器本来是为了沉淀自己的技术而做的一个技术文档,却没想到最后成了每周完成一篇blog的任务了。但是在写这篇文章的时候,我也感觉到了自己对于技术的把握还是差了一点,有些东西还是要多看、多学,很多很有用的特性都可能是自己一下子想不出来的。但是后期的文章,我将改变下方案,我会把周更变成月更,这样给自己更多的时间去对于相关的技术做一个查缺补漏。

【系统】Unity3D中的AssetBundle资源管理器(三)

前言

上一次讲到了用asset做资源管理,但是在asset的管理的时候,我们是同步把所需要的asset全部都加载出来的,前段时间看了下别人的管理机制,发现可以用unity的资源加载依赖来做这件事,就是不用我们自己加载asset,而是改用记录asset的依赖就好了。

正文

基于unity自身的依赖加载

在以前我们了解过,unity在加载一个资源的时候,如果它所依赖的assetbundle已经被打开了,那当我们加载它的时候,unity会自动把它所依赖资源一起加载出来,让asset能正常使用。在这个情况下,那当我们在根据asset来记录依赖的时候,我们只需要把它依赖的asset所在的assetbundle给加载出来,然后去对asset做一个引用计数的记录,这样的话,我们就可以省下去做加载依赖的asset这步的操作。在上一次我们所测试下来的结果,让我在这个版本做的方案就是,对assetbundle的卸载使用true这个参数,让它完全卸载掉。所以asset对于我来说就是一个做依赖关系绑定的用途。

改用异步的方式来做资源加载

在体验游戏的时候,我们可能最讨厌的就是界面卡顿,当点一个按钮需要打开一个界面的时候,我们发现,界面需要很久才出现,在等待的时候,我们会产生点击失败的感觉,这样很影响用户体验了。这种卡顿在我之前的方案里,很大一部分是因为我使用的是同步加载接口,它会阻塞住线程,等待资源全部加载回来才结束。这样的结果不是我们想要的表现方式。所以我改成用异步的方案来加载资源。但是在加载的时候,因为是异步,所以也会出现加载资源回来的顺序不一定是按照我们加载之前的顺序回来的。而且要抵消全部加载回来才显示的问题,我们可以做一个UI表现的动效,来抹平加载时间内的UI没有资源展示。

加载方案

当我们加载一个UI界面的时候,我们可以用对这个UI界面所需要的资源进行一个大的分类,如果是那种滑动列表内的资源,我们可以做一个优先级比较低的加载顺序,然后滑动列表加载进来的时候,做一个进入动画。那界面上顺序比较高的是哪个呢?我认为应该是玩家第一眼就能见到的界面,当我们在加载回来之后就可以直接显示了,那些一开始让玩家见不到面的资源,就可以在播动效的时候,再加载回来,其实不会慢很多,但是我们这样做就可以及时响应玩家的操作。我们需要给资源做一个引用列表,哪几个需要显示的prefab在引用它,还要记录一个状态,如果在加载中,所有需要引用的资源直接添加到引用列表里,如果它已经加载成功了,那就直接返回回调。

骚操作(我一般叫奇巧淫技)

在unity自带的资源依赖管理里,我们在加载出来一个prefab的时候如果它的资源没有加载成功,但是我们现实出来,它是会变紫红色的,但是,如果我们在接下来的时间内能加载出它依赖的asset所在的assetbundle的话,unity就会自动把资源赋值上去,正常现实的。所以在加载如果玩家看不到的资源,我们就可以一开始不用理会,只要第一眼能看到的资源以及加载成功了,我们就可以直接显示prefab。这里我就不知道如果出现这样的情况在unity底层会不会有cpu的消耗,一直在找丢失的资源。

卸载方案

如果我们使用了异步来做资源管理的话,会出现一个问题,就是我们如果这个资源还在加载的时候,我们就已经要把它卸载了,那我们就要在资源加载结束的时候做一个判断,这个资源是不是还用。所以我们需要给这个资源的回调做一个管理,如果不需要的话,我们就把回调事件-1,当资源加载回来之后,发现自己没有任何一个资源需要引用了,那它就把自己的状态变成卸载状态,等待下一次的回收。

资源的引用计数的问题

在之前我们所做的所有方案都是在一个理想的状态下做的,如果我们生成的一个prefab的资源,它被clone了,直接走unity的实例化,然后再把它给赋值到一个其他的地方,这个时候我们是不知道它真实的生命周期,这样的情况下,我如果默认它的生命死亡时间是跟某一界面,或者某一模块相关的话,那我们卸载了资源,它在别的地方的实体就会出现紫红色,然后出现问题。所以这种情况下,我现在想到的办法就是框架限制,资源的生命周期就限制死了,不能其他的地方使用我这个里面的资源,你要用自己去加载去。这样的话就能暂时解决了。其实这种方案也是有问题的,靠自觉,不知道什么时候就会出现问题的。只能等下一次解决了。

【系统】Unity3D中的AssetBundle资源管理器(二)

前言

在之前的那片文章上写了我们可以根据Asset的依赖,来做ab包的加载。这样可以在Asset不在用的时候及时释放内存中的资源,减少内存的峰值。也可以减少一次性回收资源太多,导致cpu峰值。

正文

设计思路

准备阶段

首先我们需要在打资源的时候做一次资源相互依赖的配置。然后把这份配置放置在StreamingAssets下,然后可以动态热更下来。在使用的时候,我们先根据ab包里总的Minifest来把每个ab包中有那些资源给读取出来,然后再根据资源依赖关系配置,我们就可以把这两个文件关联起来,asset->depedencyAssets->assetbundle。

加载阶段

根据这样的关系我们在加载的时候,当需要加载一个asset的时候,我们可以去读取它所关联的depedencyAssets资源,然后判断这个资源有在被使用,如果被使用了,我们就给他的引用计数+1;如果没有我们就根据这个asset去找它在哪个assetbundle中,然后把这个assetbundle给加载出来,再看下它所引用的asset有没有加载出来,加载出来就引用计数加1。在这里我们可以看到,只在第一次加载的时候会去给它所依赖的Asset做一次引用计数+1,因为依赖的Asset只需要去关心它是不是被依赖了,而不需要关心依赖它的那个资源被加载几次;因为只要它还在被引用,那就自然而然依赖的资源就不能被回收释放的。

卸载阶段

当我们需要卸载一块资源的时候,我们首先要做的就是,把这个资源的引用计数给-1,然后当他已经完全没有被引用了,那我们就去寻找它所依赖的资源,然后也都减1;然后我们查找这个资源所在的assetbundle包,去遍历它里面的Asset是不是都不被引用了,如果都不被引用了,我们就给这个资源做个标记要被回收的;不立刻被回收而是去做标记的好处就是,当我们的下一次还要用ab包里的资源的时候,我们可以不用再去加载ab包了,直接使用从这个ab包里拿资源,然后把这个ab包在状态给修改回去。我们将找一个空余的时间去把所有不用的ab包给回收掉。

关注点


这张图我们看到没有我们要加载的资源

这张图是我们第一次加载资源的时候它所被引用到的对象
这个时候当我们调用Resources.UnloadAsset卸载资源,它就会变成第一张图,这个资源就从内存里消失了,然后我们不去卸载ab包,第二次我们直接调用资源加载,我们看到的情况和之前是一样的。
但是,如果我们这个时候把ab包给卸载了,然后重新加载ab再加载asset之后,我们将看到这样的资源依赖图

这个时候我们再去调用Resources.UnloadAsset卸载资源你会看到资源没有被依赖,但是没有从内存中消失。

这个时候我们要卸载这个图片必须得调用assetbundle.Unload(true)来卸载,或者调用Resources.UnloadUnusedAssets()这来卸载,后一种卸载方式是会循环遍历所有的资源然后去卸载,所有会有很大的消耗;前一种的卸载方式是把资源依赖给强行解放了,所以当你实际上资源还在使用的话,就会变紫色,丢资源。
在这种Asset没有被回收的情况下,我们assetbundle没有回收,然后我们再从assetbundle中把资源加载出来,实际上使用的资源还是这个,它会被重新被引用上。但是如果我们调用了assetbundle.Unload(false)来把ab给回收了,但是没有去调用Resources.UnloadUnusedAssets()来回收asset,那你会发现这个丢失引用的资源就成了冗余资源,我再次从新加载ab,加载asset的时候,就是重新生成一份资源了

就成这样的情况了。

总结

源码地址
我们在做asset的资源管理的时候,一般来说直接调用true来做一次强行的资源回收,但是如果需要很在意表现,不想表现穿帮,对于一些资源就算冗余也无所谓的情况下,那最后调用一次Resources.UnloadUnusedAssets()来做一次资源的回收兜底行为。下一次我是做场景加载呢,还是做一个资源打包编辑器呢?看时间吧!鸽的前兆。

【编程基础】c#中的List源码解析

前言

我们在使用一些组件的时候,很容易在不了解它底层实现的情况下,使用出一些性能的问题。作为一个良好的程序员来说,能在在遇到这种问题的时候给出解决方案,算是一个基本的职业素质。所以我们需要在使用组件的时候知其所以然。

为什么需要了解List

List 这个组件是一个可伸缩的数组组件,我们会常常用它来代替数组,因为是可伸缩的,所以我们使用的地方太多了。

需要了解哪些东西

  1. 增加元素
  2. 删除元素
  3. 修改元素
  4. 怎么伸缩(这个其实在上面应该会说)

源码地址

正文

构造部分

首先我们来看下List的构造部分,源码如下:

public class List<T> : IList<T>,System.Collections.IList,IReadOnlyList<T>
{
    private const int _DefaultCapacity = 4;
    private T[] _items;
    [ContractPublicPropertyName("Count")]
    private int  _size;
    private int _version;
    [NoSerialized]
    private Object _syncRoot;
    static readonly T[] _emptyArray = new T[0];
    public List(){
        _items = _emptyArray;
    }
    public List(int capacity){
        if(capacity<0) ThrowHelper.ThrowArgumentOutOfRangeException(ExcptionArgument.capacity,ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
        Contract.EndContractBlock();
        if(capacity == 0)
            _items = _emptyArray;
        else
            _items = new T[capacity];
    }
    public List(IEnumerable<T> collection){
        if (collection==null)
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
            Contract.EndContractBlock();
            ICollection<T> c = collection as ICollection<T>;
            if( c != null) {
                int count = c.Count;
                if (count == 0)
                {
                    _items = _emptyArray;
                }
                else {
                    _items = new T[count];
                    c.CopyTo(_items, 0);
                    _size = count;
                }
            }
            else {
                _size = 0;
                _items = _emptyArray;
                using(IEnumerator<T> en = collection.GetEnumerator()) {
                    while(en.MoveNext()) {
                        Add(en.Current);
                    }
                }
            }
        //...
        //其他内容
    }
}

从源码中我们可以知道,List继承IList,和IReadOnlyList这两个接口,IList是提供主要的接口,IReadOnlyList提供了迭代接口。
我们还能知道List内部是用数组来实现的,它有三种构造方法,一个是啥都不传的,默认初始化一个长度0的数组,一个是初始化数组长度的构造函数,一个是复制给定集合内容的构造函数,长度和内容都和传进来的数组一样。

添加

当我们看到第三种构造函数的时候,我们会发现最下面有一个Add这个函数的调用,这个函数就是List中添加。
源码如下:

public void Add(T item){
    if(_size == _item.Length) ExsureCapacity(_size + 1);
    _item[_size++] = item;
    _version++;
}
private void EnsureCapacity(int min){
    if(_item.Length < min){
        int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;
        if((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
        if(newCapacity < min) newCapacity = min;
        Capacity = newCapacity;
    }
}
public int Capacity{
    get{
        Contract.Ensures(Contract.Result<int>() >= 0);
        return _item.Length;
    }
    set{
        if(value < _size){
            ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
        }
        Contract.EndContractBlock();
        if(value != _items.Length){
            if(value > 0){
                T[] newItems = new T[value];
                if(_size > 0){
                    Array.Copy(_items,0,newItems,0,_size);
                }
                _items = newItems;
            }else{
                _items = _emptyArray;
            }
        }
    }
}

从上面的源码我们可以看出来,当List在Add一个item的时候,会先去判断这个数组是不是已经满了,如果满了就拓容,从4,16,32 ...n(上一次数组大小)*2 这样的大小来拓容的,当拓容之后,会新建一个新的数组,把原来数组内容给拷贝进去,然后再去添加新的元素。

  1. 如果你直接调用Capacity赋值数组商都为负数的时候,你会出现数组被清理成默认的,但是数组的标记size(这个代表当前添加到数组第几个位置)却没有清空,下一次你要用的时候,会浪费之前那么多的内存空间。

  2. 当你在添加一堆数据的时候(例如128个数据)在一开始没有给List赋值长度的话,它就会在你添加的时候进行六次扩容,然后这六次扩容中有五次是需要拷贝元素数据的,这样每次拷贝都会造成新的内存垃圾,这样就会给GC带来很多负担。

  3. 如果添加的数量不得当,也会照成内存空间的浪费,比如元素数量为520的时候,List就会扩容到1024个元素,这样就会多出502个空间单位没有被使用。

删除

删除我们主要看Remove这个函数
源码如下:

public bool Remove(T item){
    int index = indexOf(item);
    if (index>=0){
        RemoveAt(index);
        return true;
    }
    return false;
}
public int indexof(T item){
    Contract.Ensures(Contract.Result<int>() >= -1);
    Contract.Ensures(Contract.Result<int>() < Count);
    return Array.IndexOf(_items,item,0,_size);
}
public void RemoveAt(int index){
    if((uint)index >= (uint)_size){
        ThrowHelper.ThrowArgumentOutOfRangeException();
    }
    Contract.EndContractBlock();
    _size--;
    if(index < _size){
        Array.Copy(_items,index+1,_items,index,_size-index);
    }
    _item[size] = default(T);
    _version++;
}

Remove接口中包含了IndexOf和RemoveAt,这两个方法,IndexOf是为了定位要删除的元素的位置,这个接口的实现是按索引从0到n个位置做意义比较,时间复杂度为O(n);RemoveAt这个方法是用来删除指定位置的元素,但是它的实现是用Array.Copy来进行数组的覆盖。

插入

源码如下:

public void Insert(int index,T item){
    if((uint) index > (uint)_size){
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert);
    }
    Contract.EndContractBlock();
    if(_size == _items.Length) EnsureCapacity(_size + 1);
    if(index < _size){
        Array.Copy(_items,index,_items,index+1,_size-index);
    }
    _items[index] = item;
    _size++;
    _version++;
}

从源码中我们可以看到,Insert和Add接口一样,都会先检查容量是否足够,如果不够就扩容,插入的位置是否是数组的最后,如果不是进行数组拷贝,将index后面的元素集体后移一位。
在这里我们可以很清楚一个概念,就是最坏的情况下,我们插入一个数据需要做2次数据拷贝,会造成GC时的压力。

思考

其他相关的接口比如AddRange,RemoveRange,InsertRange的原理其实和Add,Remove,Insert一样,区别就是从单个元素变成以容易为单位的操作,都是做一些重复覆盖的操作,和扩容。他们没有进行任何形式的优化,都是使用顺序迭代的方式。如果频繁调用,会照成不小的内存冗余和GC的压力。

其他的一些接口说明

比如[]的实现

public T this[int index] {
            get {
                // Following trick can reduce the range check by one
                if ((uint) index >= (uint)_size) {
                    ThrowHelper.ThrowArgumentOutOfRangeException();
                }
                Contract.EndContractBlock();
                return _items[index]; 
            }

            set {
                if ((uint) index >= (uint)_size) {
                    ThrowHelper.ThrowArgumentOutOfRangeException();
                }
                Contract.EndContractBlock();
                _items[index] = value;
                _version++;
            }
        }

这个就是直接使用数组的的索引方式来获取元素,赋值元素,

Clear清除接口

public void Clear() {
            if (_size > 0)
            {
                Array.Clear(_items, 0, _size); // Don't need to doc this but we clear the elements so that the gc can reclaim the references.
                _size = 0;
            }
            _version++;
        }

调用的时候不会去删除数组内的元素,只是将元素清零,然后设置_size为0;

Contains接口

public bool Contains(T item) {
   if ((Object) item == null) {
                for(int i=0; i<_size; i++)
                    if ((Object) _items[i] == null)
                        return true;
                return false;
            }
            else {
                EqualityComparer<T> c = EqualityComparer<T>.Default;
                for(int i=0; i<_size; i++) {
                    if (c.Equals(_items[i], item)) return true;
                }
                return false;
            }
        }

我们看到它就是做了一次线性比较的方式来对数组进行迭代查找,并且返回的是第一个比对上的元素。

ToArray接口

public T[] ToArray() {
            Contract.Ensures(Contract.Result<T[]>() != null);
            Contract.Ensures(Contract.Result<T[]>().Length == Count);

            T[] array = new T[_size];
            Array.Copy(_items, 0, array, 0, _size);
            return array;
        }

它的实现就是新建一个数组,然后把元素拷贝进去再返回出来。

Find 接口

 public T Find(Predicate<T> match) {
            if( match == null) {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
            }
            Contract.EndContractBlock();

            for(int i = 0 ; i < _size; i++) {
                if(match(_items[i])) {
                    return _items[i];
                }
            }
            return default(T);
        }

同样适用的是线性查找,对每个元素都进行比较,时间复杂度为O(n).

Enumerator 迭代的实现

public Enumerator GetEnumerator() {
            return new Enumerator(this);
        }
IEnumerator<T> IEnumerable<T>.GetEnumerator() {
            return new Enumerator(this);
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
            return new Enumerator(this);
        }
public struct Enumerator : IEnumerator<T>, System.Collections.IEnumerator
        {
            private List<T> list;
            private int index;
            private int version;
            private T current;

            internal Enumerator(List<T> list) {
                this.list = list;
                index = 0;
                version = list._version;
                current = default(T);
            }

            public void Dispose() {
            }

            public bool MoveNext() {

                List<T> localList = list;

                if (version == localList._version && ((uint)index < (uint)localList._size)) 
                {                                                     
                    current = localList._items[index];                    
                    index++;
                    return true;
                }
                return MoveNextRare();
            }

            private bool MoveNextRare()
            {                
                if (version != list._version) {
                    ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
                }

                index = list._size + 1;
                current = default(T);
                return false;                
            }

            public T Current {
                get {
                    return current;
                }
            }

            Object System.Collections.IEnumerator.Current {
                get {
                    if( index == 0 || index == list._size + 1) {
                         ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
                    }
                    return Current;
                }
            }

            void System.Collections.IEnumerator.Reset() {
                if (version != list._version) {
                    ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
                }

                index = 0;
                current = default(T);
            }

        }

我们可以看到Enumerator这个接口在每次被获取的时候都是new出来的,如果使用大量的迭代器的时候就会有大量的对象,照成GC压力

Sort接口

public void Sort()
        {
            Sort(0, Count, null);
        }
 public void Sort(IComparer<T> comparer)
        {
            Sort(0, Count, comparer);
        }
public void Sort(int index, int count, IComparer<T> comparer) {
            if (index < 0) {
                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
            }

            if (count < 0) {
                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
            }

            if (_size - index < count)
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
            Contract.EndContractBlock();

            Array.Sort<T>(_items, index, count, comparer);
            _version++;
        }

        public void Sort(Comparison<T> comparison) {
            if( comparison == null) {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
            }
            Contract.EndContractBlock();

            if( _size > 0) {
                IComparer<T> comparer = new Array.FunctorComparer<T>(comparison);
                Array.Sort(_items, 0, _size, comparer);
            }
        }
// Array.Sort
internal void Sort(int left, int length)
            {
#if FEATURE_CORECLR
                // Since QuickSort and IntrospectiveSort produce different sorting sequence for equal keys the upgrade 
                // to IntrospectiveSort was quirked. However since the phone builds always shipped with the new sort aka 
                // IntrospectiveSort and we would want to continue using this sort moving forward CoreCLR always uses the new sort.

                IntrospectiveSort(left, length);
#else
                if (BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
                {
                    IntrospectiveSort(left, length);
                }
                else
                {
                    DepthLimitedQuickSort(left, length + left - 1, IntrospectiveSortUtilities.QuickSortDepthThreshold);
                }
#endif
            }
             private void DepthLimitedQuickSort(int left, int right, int depthLimit)
            {
                // Can use the much faster jit helpers for array access.
                do
                {
                    if (depthLimit == 0)
                    {
                        // Add a try block here to detect IComparers (or their
                        // underlying IComparables, etc) that are bogus.
                        try
                        {
                            Heapsort(left, right);
                            return;
                        }
                        catch (IndexOutOfRangeException)
                        {
                            throw new ArgumentException(Environment.GetResourceString("Arg_BogusIComparer", comparer));
                        }
                        catch (Exception e)
                        {
                            throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), e);
                        }
                    }

                    int i = left;
                    int j = right;

                    // pre-sort the low, middle (pivot), and high values in place.
                    // this improves performance in the face of already sorted data, or 
                    // data that is made up of multiple sorted runs appended together.
                    int middle = GetMedian(i, j);

                    // Add a try block here to detect IComparers (or their
                    // underlying IComparables, etc) that are bogus.
                    try
                    {
                        SwapIfGreaterWithItems(i, middle); // swap the low with the mid point
                        SwapIfGreaterWithItems(i, j);      // swap the low with the high
                        SwapIfGreaterWithItems(middle, j); // swap the middle with the high
                    }
                    catch (Exception e)
                    {
                        throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), e);
                    }
                    Object x = keys[middle];
                    do
                    {
                        // Add a try block here to detect IComparers (or their
                        // underlying IComparables, etc) that are bogus.
                        try
                        {
                            while (comparer.Compare(keys[i], x) < 0) i++;
                            while (comparer.Compare(x, keys[j]) < 0) j--;
                        }
                        catch (IndexOutOfRangeException)
                        {
                            throw new ArgumentException(Environment.GetResourceString("Arg_BogusIComparer", comparer));
                        }
                        catch (Exception e)
                        {
                            throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), e);
                        }
                        Contract.Assert(i >= left && j <= right, "(i>=left && j<=right)  Sort failed - Is your IComparer bogus?");
                        if (i > j) break;
                        if (i < j)
                        {
                            Object key = keys[i];
                            keys[i] = keys[j];
                            keys[j] = key;
                            if (items != null)
                            {
                                Object item = items[i];
                                items[i] = items[j];
                                items[j] = item;
                            }
                        }
                        i++;
                        j--;
                    } while (i <= j);

                    // The next iteration of the while loop is to "recursively" sort the larger half of the array and the
                    // following calls recrusively sort the smaller half.  So we subtrack one from depthLimit here so
                    // both sorts see the new value.
                    depthLimit--;

                    if (j - left <= right - i)
                    {
                        if (left < j) DepthLimitedQuickSort(left, j, depthLimit);
                        left = i;
                    }
                    else
                    {
                        if (i < right) DepthLimitedQuickSort(i, right, depthLimit);
                        right = j;
                    }
                } while (left < right);
            }

我们可以看到使用的排序方法是用的快排,所以它的时间复杂度为O(nlogn)

总结

在分析完之后,我们可以知道List的效率并不高,只是通用性强,方便而已,大部分的算法都是使用线性复杂度的算法,这种算法遇到规模比较大的计算量级的时候就会照成大量的CPU的消耗。而且它的内存分配方式极其的不合理,只要元素不断的增加就会多次new数组,然后抛弃原来的数组,造成GC压力。所以List是一个好用但不高效的组件。

【编程基础】Unity的内存管理

前言

最近在看完unity技术开放日中高川老师对于内存的讲解,对unity的内存有了一些全新的认识。写一篇文章来做一次学习总结

正文

Unity的内存结构

在unity中内存其实分为两块:

  1. Native Memory 原生内存

  2. Managed Memory 托管内存

分为这两块主要是因为Unity的底层是用c++所写的所以有一块Native Memory,业务层使用Mono为了能够跨平台所以在mono中的内存是Managed Memory。所以我们在考虑Unity的内存的时候,要分别讨论这两块内存的管理机制(分配,回收)

Native Memory

介绍

Unity实际上是一个C++引擎,所有的实体最终都会反映到C++上,也就是反映在Native Memory上,当我们使用Unity的组件的时候,我们就可以很明显的看到Native Memory的上升。所以在使用Unity的组件的时候,我们需要好好考虑Native Memory的内存变化,来进行一个正确的使用方案。

分配方案

New/MALLOC

  1. New是操作符,MALLOC是函数

  2. New从自由存储区(free store)上分配,MALLOC从堆上分配。凡是通过New进行的内存申请,就是自由存储区,它可以是堆上的,也可以是今天存储区的,还能使用New的时候不给对象分配内存,只是返回一个指针实参;MALLOC分配的堆是操作系统中的术语,是操作系统所维护的一块特殊内存。

  3. New内存分配成功时是放回对象的类型指针,类型严格与对象匹配,符合类型安全性的操作符,它不会试图访问自己没被授权的内存区域;Malloc分配成功返回的类型是void*,需要强制类型转换到我们需要的类型

  4. New内存分配失败时候,会抛出bac_alloc异常,不会返回Null;Malloc分配失败会返回Null。

  5. new不需要指定内存大小,编译器会自动计算出来;而Malloc需要显式的指出内存的大小。

  6. new分配经历三个步骤:第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间来储存制定类型对象;第二:编译器运行相应的构造函数一构造对象;第三:构造成功后,返回一个指向该对象的指针;当被回收的时候,调用delete的时候会经历两个步骤:第一:调用对象的析构函数;第二:编辑器调用operator delete(数组调用operator delete[])函数释放内存空间。Malloc不会调用构造函数。

  7. new对数组的是可以在初始化的时候调用构造函数去初始化每一个元素,释放的时候可以对每个对象调用析构函数。malloc 在分配内存的时候,不知道你再内存上要放的是什么。

  8. new可以在operator new/operator delete的实现调用malloc的;但是malloc的实现不可以去调用new。

  9. new的operator new/operator delete是可以被重载的。malloc是不可以的。

  10. malloc分配内存后,如果使用的时候内存不足,它是可以调用realloc函数去重新分配内存扩容。new就不能这样直管的去扩容。

  11. New抛出异常的时候会调用用户的指定错误处理函数;malloc在内存不足的时候只会返回一个null

Unity重写New/Malloc

unity会定义一些宏定义,然后当调用New/Malloc分配内存的时候,不会去调用库函数,而是使用unity自己实现的函数。

如图所示当Unity在分配内存的时候会定义这个分配内存的类型(Memory Label),然后根据这个Memory Label来判断在哪里分配内存给用户。其中有stack Allocator(栈分配器),Batch Allocator(批量分配器),DynamicHeapAllocator(动态堆分配器)

Stack Allocator

这一块其实不是我们一般认为的栈帧,而是unity自己实现的一个栈类型的内存分配方案。他其实是用来做一些临时数据的内存分配,要十分快的不被使用了。

特性

Fast(快),Small(小),TemPorary(临时)

结构

如上图所示,一块数据包含header,user这两块内容

  1. header

这块数据保存着这个内存块的一些信息,比如是否被删除,user的大小,上一块的位置。

  1. user

这块就是用户实际使用到的数据。

使用方案

  1. 这一整个栈分配器,在程序启动的时候就定好是多大了,一般在编辑器模式下,主线程16M,子线程256kb,在runtime(运行)模式下主线程128kb-1M,子线程 65kb。

  2. 这个是通过移动栈顶指针的方式来做分配和回收的;当用户要分配一块内存的时候,直接在栈顶指针之下分配一块内存,然后把栈顶指针给下移;当回收的时候,先在header上标记删除,然后看这块内存是不是栈顶指针指向的那块内存,如果是,就把栈顶指针上移,检测上一块内存是不被标记回收,标记了就回收,没有就停在这块。在这里就会有一个问题出现,如果我回收的是中间那块内存,就会只标记,不做栈顶指针的移动,下次使用的时候,还是去分配新的内存,而没有对这块内存重复利用了,只有等下一次栈顶指针移动到这里才会回收重复利用。所以这样就产生了内存碎片,内存浪费。

  3. 如果需要分配的内存已经超出了栈分配器的剩余可用的内存大小的时候(这里指的是栈顶指针之下的内存),会把内容分配到DynamicHeapAllocator(动态堆分配器)中抛出一个MemoryManager.FallbackAllocation这个异常。在栈分配器可能分配一段内存零点几毫秒但是在动态堆分配里,它可能需要几毫秒,这个时间消耗飙升。

提醒

现在说完了栈分配器的一些特性,我们就可以对它的一些特性做一个解释了,为啥快呢?因为它只做指针的移动,而不会去做其他的操作,为啥需要临时呢?因为你不能永久的保持栈顶指针不动,而一直在回收中间部分的内存,这样就内存无法重复利用,引起内存碎片。造成很大的浪费,所以你需要快速的用完就丢掉。

未完待续

关于其他的内存分配器,等下回分享

Managed Memory

介绍

这个是托管内存,关于这块的内容网上到处都有,各种GC就是大部分说的就是这个托管内存的回收机制,它就是为了方便使用者不去做内存的管理,只需要使用,使用完了编辑器就会自动帮你回收内存,用的时候也是编辑器自动给你分配一块内存给你。
主流的GC算法:

  1. 保守内存回收(BoehmGC等)

  2. 分代式内存回收(sGen)

  3. 引用式内存回收(java)

关于GC的一些算法,我之后再详细写,现在先写一个mono中的BoehmGC,为啥mono里要用BoehmGC呢,而不用市场上现在很流行的分代GC算法呢?这个除了一些历史原因,还有一个原因是因为使用分代GC你会做一个内存块的移动,和对当前内存块的活跃度的判断;这些操作都会对cpc的计算力做一次消耗。

BoehmGC

这个GC算法是从c++中先使用的,所以带有很强烈的c++的一些特性,你可能在后面结合c++的一些特性能更好理解它有些东西为啥这样做的。

从上图我们可以看到它的内存管理属于一个二级管理,第一级代表的这个kind的类型,常见的有 PTRFREE(无指针类型),Normal(比较一般的类型),还有 Uncollectable(不可回收类型);一般Boehm自己用到的会放在Uncollectable中。然后再下面你看到它有一个size的二级,这里代表的是当前它下面所挂的内存的大小,它会把所有和自己所表示大小一样的内存块通过链表来链接在自己的下面。

分配

当用户要分配一个8byte大小的内存的时候,因为现在表面的最小是16,所以它就会把16下的内存块拿出一个给用户,剩下的8byte就直接浪费了。如果16byte的内存链表下面没有可用的内存块的时候,它就会从更大一级的内存块(32)中拿一块出来,然后一分为二16byte用户拿去用,剩下的16byte挂载16byte链表下。

回收

当用户要回收一块内存的会去找它物理相连的地址上的数据是不是也被回收了,如果也被回收了,那他们两个就合并在一起,然后挂在一个更大的链表下面等待使用(比如两个Block0),减少内存碎片。

标记回收

标记回收是怎么去找我哪块内存需要回收,在Boehm里它是属于非精准性回收。表示分配的内存没人用但是不一定内被标记到可以回收,还有一种是没人用的内存也没有办法被使用;说明这两种的内存被认为还在被引用。

我们看上图,对象的引用是因为在一个地方保存了他的地址,所以被认为是被引用了。在Boehm中他认为保存的这个数据是指针,它靠的是猜,怎么猜的呢?它会保存自己从HEAP分配内存的最低地址和最高地址,就好比现在我们看到的0x012它可能是一个数,但是它刚好在这个区间内,那不好意思了,你就是指针了,那通过你指向的位置的对象,也就被我引用了,那你就不能别回收了。还有一个0x013(也认为是一个数)如果把它也刚好在这个区间,但是它直接指向的那个地址其实是一个没人用到的(或者已经被回收的),Boehm就会把它标记到黑名单里,其他地方在使用内存的时候刚好踩到这块内存,Boehm会给你分配别的地方的内存,这块现在就是不能用了。
所以当你分配了一堆小内存的时候,那这个时候,你产生黑名单的概率和不能回收的概率都会大大提高的。

总结

在使用内存的时候,我们需要优先考虑分配大内存,然后再去分配小内存;减少内存碎片的,降低黑名单,以及不能回收的概率;也可以在大内存被回收之后可以被重复切成小块内存使用。但是当你有一堆小内存,他们的物理内存地址不是连续的时候,就算你能真实大小满足这一次要使用的内存大小的时候,但是因为你不是连续的,所以不能用,导致系统需要从新分配一块连续的内存给用户使用。