版权声明:本文为博主原创文章,欢迎转载。请保留博主链接:http://blog.csdn.net/andrewfan

上篇文章《【Unity优化】Unity中究竟能不能使用foreach?》发表之后,曾经有网友说,在他的不同的Unity版本上,发现了泛型List无论使用foreach还是GetEnumerator均会产生GC的情况,这就有点尴尬了。由于它本身就是Mono编译器和相应.net库才能决定的原因,这就使得在使用系统提供的List时,又能最终摆脱GC的纠缠变得很困难。于是抓耳挠腮,翻出差不多六七年为Java代码写的动态数组,然后大肆修改一番。最终好像终于逃离GC的魔咒。

先奉上代码:

自定义的List

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine; namespace AndrewBox.Math
{
/// <summary>
/// 动态数组
/// @author AndrewFan
/// </summary>
/// <typeparam name="T">任意类型</typeparam>
public class AB_List<T> :IEnumerable<T>
{
protected int m_capacity=10; // 容量
protected T[] m_items;// 内部数组
protected int m_length;// 存放的单元个数
protected int m_mayIdleID;// 可能空闲的单元下标 protected IEnumerator<T>[] m_enumerators; //枚举器组
protected bool[] m_enumStates;//枚举器组当前占用状态
public AB_List()
{
init(5);
}
public AB_List(int capacity,int enumCount=5)
{
init(enumCount);
}
protected void init(int enumCount)
{
m_capacity = m_capacity<10?10:m_capacity;
enumCount = enumCount < 5 ? 5 : enumCount;
m_items = new T[m_capacity];
if (m_enumerators == null)
{
m_enumerators = new IEnumerator<T>[enumCount];
m_enumStates = new bool[enumCount];
for (int i = 0; i < m_enumerators.Length; i++)
{
m_enumerators[i] = new ABEnumerator<T>(this,i);
}
}
}
/// <summary>
/// 增加单元
/// </summary>
/// <param name="element">添加的单元</param>
public virtual void Add(T element)
{
increaseCapacity();
// 赋值
m_items[m_length] = element;
m_length++;
} /// <summary>
/// 插入单元
/// </summary>
/// <param name="index">插入位置</param>
/// <param name="element">单元</param>
/// <returns>操作是否成功</returns>
public virtual bool Insert(int index, T element)
{
if (index < 0)
{
return false;
}
if (index >= m_length)
{
Add(element);
return true;
}
increaseCapacity();
// 向后拷贝
// for(int i=length;i>index;i--)
// {
// datas[i]=datas[i-1];
// }
System.Array.Copy(m_items, index, m_items, index + 1, m_length - index); m_items[index] = element; m_length++;
return true;
} public virtual T this[int index]
{
get
{
//取位于某个位置的单元
if (index < 0 || index >= m_length)
{
throw new InvalidOperationException();
}
return m_items[index];
}
set
{
//设置位于某个位置的单元
if (index < 0 || index >= m_length)
{
throw new InvalidOperationException();
}
m_items[index] = value;
}
} /// <summary>
/// 增长容量
/// </summary>
protected void increaseCapacity()
{
if (m_length >= m_capacity)
{
int newCapacity = m_capacity;
if(newCapacity == 0)
{
newCapacity++;
}
newCapacity *= 2;
T[] datasNew = new T[newCapacity];
System.Array.Copy(m_items, 0, datasNew, 0, m_length);
m_items = datasNew;
m_capacity = newCapacity;
}
}
/// <summary>
/// 清空单元数组
/// </summary>
public virtual void Clear()
{
for (int i = 0; i < m_length; i++)
{
m_items[i] = default(T);
}
m_length = 0;
} /// <summary>
/// 是否包含某个单元
/// </summary>
/// <param name="element">单元</param>
/// <returns>是否包含</returns>
public bool Contains(T element)
{
for (int i = 0; i < m_length; i++)
{
if (m_items[i].Equals(element))
{
return true;
}
}
return false;
} /// <summary>
/// 获取指定单元在当前列表中的位置,从前向后查找
/// </summary>
/// <param name="element">单元</param>
/// <returns>位置</returns>
public int IndexOf(T element)
{
for (int i = 0; i < m_length; i++)
{
if (m_items[i].Equals(element))
{
return i;
}
}
return -1;
}
/// <summary>
/// 获取指定单元在当前列表中的位置,从后先前查找
/// </summary>
/// <param name="element">单元</param>
/// <returns>位置</returns>
public int LastIndexOf(T element)
{
for (int i = m_length-1; i >=0; i--)
{
if (m_items[i].Equals(element))
{
return i;
}
}
return -1;
} /// <summary>
/// 获得长度
/// </summary>
public virtual int Count
{
get
{
return m_length;
}
}
/// <summary>
/// 移除指定位置的单元,如果单元归属权属于当前列表,则会将其卸载
/// </summary>
/// <param name="index">位置索引</param>
/// <returns>移除掉的单元</returns>
public virtual void RemoveAt(int index)
{
if (index < 0 || index >= m_length)
{
return;
}
for (int i = index; i <= m_length - 2; i++)
{
m_items[i] = m_items[i + 1];
}
m_length--;
}
/// <summary>
/// 移除指定尾部单元
/// </summary>
/// <returns>移除掉的单元</returns>
public virtual T RemoveEnd()
{
if (m_length <= 0)
{
return default(T);
}
T temp = m_items[m_length - 1];
m_items[m_length - 1] = default(T);
m_length--;
return temp;
}
/// <summary>
/// 从指定位置开始(包括当前),移除后续单元,如果单元归属权属于当前列表,则会将其卸载
/// </summary>
/// <param name="index">要移除的位置</param>
/// <param name="innerMove">是否是内部移动</param>
/// <returns>被移除的个数,如果index越界,则返回-1</returns>
public virtual int RemoveAllFrom(int index)
{
if (index < 0 || index >= m_length)
{
return -1;
}
int removedNum = 0;
for (int i = m_length - 1; i >= index; i--)
{
m_items[i] = default(T);
m_length--;
removedNum++;
}
return removedNum;
}
/// <summary>
/// 移除指定单元,如果单元归属权属于当前列表,则会将其卸载
/// </summary>
/// <param name="element">单元</param>
/// <returns>是否操作成功</returns>
public virtual bool Remove(T element)
{
int index = IndexOf(element);
if (index < 0)
{
return false;
}
RemoveAt(index);
return true;
}
/// <summary>
/// 获取所有数据,注意这里的数据可能包含了很多冗余空数据,长度>=当前数组长度。
/// </summary>
/// <returns>所有数据数组</returns>
public T[] GetAllItems()
{
return m_items;
}
/// <summary>
/// 转换成定长数组,伴随着内容拷贝。
/// 如果是值类型数组,将与本动态数组失去关联;
/// 如果是引用类型数组,将与本动态数组保存相同的引用。
/// </summary>
/// <returns>数组</returns>
public virtual Array ToArray()
{
T[] array = new T[m_length];
for (int i = 0; i < m_length; i++)
{
array[i] = m_items[i];
}
return array;
}
/// <summary>
/// 显示此数组,每个单元之间以逗号分隔
/// </summary>
public void Show()
{
string text = "";
for (int i = 0; i < m_length; i++)
{
T obj = m_items[i];
text += (obj.ToString() + ",");
}
Debug.Log(text);
} /// <summary>
/// 显示此数组,每个单元一行
/// </summary>
public void ShowByLines()
{
string text = "";
for (int i = 0; i < m_length; i++)
{
T obj = m_items[i];
text += (obj.ToString());
}
Debug.Log(text);
} public IEnumerator<T> GetEnumerator()
{
//搜索可用的枚举器
int idleEnumID = -1;
for (int i = 0; i < m_enumStates.Length; i++)
{
int tryID=i+m_mayIdleID;
if (!m_enumStates[tryID])
{
idleEnumID = tryID;
break;
}
}
if (idleEnumID < 0)
{
Debug.LogError("use too much enumerators");
}
//标记为已经使用状态
m_enumStates[idleEnumID] = true;
m_enumerators[idleEnumID].Reset();
//向前迁移空闲坐标
m_mayIdleID = (m_mayIdleID + 1) % m_enumStates.Length;
return m_enumerators[idleEnumID];
}
IEnumerator IEnumerable.GetEnumerator()
{
return null;
} struct ABEnumerator<T> : IDisposable, IEnumerator<T>
{
private AB_List<T> m_list;
private int m_idNext;
private T m_current;
private int m_id;
public object Current
{
get
{
if (this.m_idNext <= 0)
{
throw new InvalidOperationException();
}
return this.m_current;
}
}
T IEnumerator<T>.Current
{
get
{
return this.m_current;
}
} internal ABEnumerator(AB_List<T> list,int id)
{
this.m_list = list;
this.m_idNext = 0;
this.m_id=id;
m_current = default(T);
} void IEnumerator.Reset()
{
this.m_idNext = 0;
} public void Dispose()
{
//this.m_list = null;
//清除使用标记
m_list.m_enumStates[m_id] = false;
m_list.m_mayIdleID = m_id;
} public bool MoveNext()
{
if (this.m_list == null)
{
throw new ObjectDisposedException(base.GetType().FullName);
}
if (this.m_idNext < 0)
{
return false;
}
if (this.m_idNext < this.m_list.Count)
{
this.m_current = this.m_list.m_items[this.m_idNext++];
return true;
}
this.m_idNext = -1;
return false;
}
}
} }

下面是修改后的ForeachTest 类

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using AndrewBox.Math; public class ForeachTest : MonoBehaviour { int[] m_intArray;
List<int> m_intList;
ArrayList m_arryList;
AB_List<int> m_intABList;
public void Start ()
{
m_intArray = new int[2];
m_intList = new List<int>();
m_arryList = new ArrayList();
m_intABList = new AB_List<int>();
for (int i = 0; i < m_intArray.Length; i++)
{
m_intArray[i] = i;
m_intList.Add(i);
m_arryList.Add(i);
m_intABList.Add(i);
}
} void Update ()
{
testABListEnumLevel();
//testABListGetEmulator();
//testABListForeach();
} void testIntListForeach()
{
for (int i = 0; i < 1000; i++)
{
foreach (var iNum in m_intList)
{
}
}
}
void testIntListGetEmulator()
{
for (int i = 0; i < 1000; i++)
{
var iNum = m_intList.GetEnumerator();
while (iNum.MoveNext())
{
}
}
}
void testIntArrayForeach()
{
for (int i = 0; i < 1000; i++)
{
foreach (var iNum in m_intArray)
{
}
}
}
void testIntArrayGetEmulator()
{
for (int i = 0; i < 1000; i++)
{
var iNum = m_intArray.GetEnumerator();
while (iNum.MoveNext())
{
}
}
}
void testArrayListForeach()
{
for (int i = 0; i < 1000; i++)
{
foreach (var iNum in m_arryList)
{
}
}
}
void testArrayListGetEmulator()
{
for (int i = 0; i < 1000; i++)
{
var iNum = m_arryList.GetEnumerator();
while (iNum.MoveNext())
{
}
}
}
void testABListForeach()
{
for (int i = 0; i < 1000; i++)
{
foreach (var iNum in m_intABList)
{
}
}
}
void testABListGetEmulator()
{
for (int i = 0; i < 1000; i++)
{
using (var iNum = m_intABList.GetEnumerator())
{
while (iNum.MoveNext())
{
var t = iNum.Current;
}
}
}
}
void testABListEnumLevel()
{
foreach (var iNum1 in m_intABList)
{
foreach (var iNum2 in m_intABList)
{
foreach (var iNum3 in m_intABList)
{
foreach (var iNum4 in m_intABList)
{
//foreach (var iNum5 in m_intABList)
//{
//}
}
}
}
}
}
}

Foreach调用解析

关键之处作个解释:

首先理清楚IEnumerable、IEnumerator之间的关系。

IEnumerable是指那种可以被枚举的列表类型,如果我们自己自定义一个List,希望它能结合foreach使用的话,必须实现这个接口。

IEnumerator是一个枚举器。

系统库里的IEnumerable接口是这样:

using System.Runtime.InteropServices;

namespace System.Collections
{
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
}

在我的ABList类中实现接口的函数是下面这样:

        IEnumerator  IEnumerable.GetEnumerator()
{
if (m_enumerator == null)
{
m_enumerator = new ABEnumerator<T>(this);
}
m_enumerator.Reset();
return m_enumerator;
}

目前的函数实现经过设计的话,它不会产生GC。然而,问题在后面紧紧跟随。实现了IEnumerable接口之后。当我们使用形如foreach(var t in list)的时刻,它就会去调用list中的继承于IEnumerator的Current实现:

namespace System.Collections
{
public interface IEnumerator
{
object Current
{
get;
} bool MoveNext();
void Reset();
}
}

看到这里,它返回的是object,如果我们List中存放的是值类型,那么系统自然就产生了一次box装箱操作,GC于是悄悄地产生了。

也正是因为这个原因,微软后来加入了泛型的IEnumerator。但是,为了兼容以前的设计,这个泛型IEnumerator被设计成实现于之前的IEnumerator,而它的下方增加了同样的Current的Get方法。

using System;
using System.Collections; namespace System.Collections.Generic
{
public interface IEnumerator<T> : IEnumerator, IDisposable
{
T Current
{
get;
}
}
}

同样的设计也被用于泛型的IEnumerable,

using System.Collections;

namespace System.Collections.Generic
{
public interface IEnumerable<T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
}

如果我们实现泛型的IEnumerable和IEnumerator,必须同时泛型和非泛型的GetEnumerator和Current方法。

那么,问题来了。现在有两个GetEnumerator()方法,两个Current的Get方法,究竟该用谁的呢?

首先,在实现的时候就需要加以区分:

        public IEnumerator<T> GetEnumerator()
IEnumerator IEnumerable.GetEnumerator()

这两个实现,你给其中一个加上public,另外一个就不能加上public;两个函数至少有一个需要增加[接口名称.]这种前缀;那么最终我们在foreach期间调用的就是public的那个方法。

自然,我们这里为了避免使用到非泛型IEnumerator中的Current方法的object返回形式,我们必须使用将唯一的生存权留给泛型的GetEnumerator。

同样,我们也需要在自定义的枚举器中作出选择。保留泛型的Current函数。

 struct ABEnumerator<T> : IDisposable, IEnumerator<T>
{
private AB_List<T> m_list;
private int m_idNext;
private T m_current; public object Current
{
get
{
if (this.m_idNext <= 0)
{
throw new InvalidOperationException();
}
return this.m_current;
}
}
T IEnumerator<T>.Current
{
get
{
return this.m_current;
}
}
...

GC测试

应用于AB_List< int >的foreach

    void testABListForeach()
{
for (int i = 0; i < 1000; i++)
{
foreach (var iNum in m_intABList)
{
}
}
}

没有产生GC

应用于AB_List< int >的GetEnumerator

    void testABListGetEmulator()
{
for (int i = 0; i < 1000; i++)
{
var iNum = m_intABList.GetEnumerator();
while (iNum.MoveNext())
{
var t= iNum.Current;
}
}
}

也没有产生GC

局限性

本list虽然没有GC,但是其使用也有一定的局限性。

最大的局限性是foreach嵌套的问题。当我们在很多重嵌套中同时foreach这个list时,相当于需要多个枚举器同时存在。List被设计为存储一定数目的枚举器组,以便于多次调用。(这样设计是为了避免值类型枚举器被当做引用返回时造成的装箱问题)

不过,一般而言,你应该不需要那么多层的foreach嵌套,默认允许同时嵌套5层,如果你需要超过5层的嵌套,则在ABList的构造函数中传入更多测层次就可以。

另外一个小小的限制就是,当你使用getEnumerator()时,需要把它们放在using中,大概是这样:

            using (var iNum = m_intABList.GetEnumerator())
{
}

这样做的目的是在代码块结束时,调用枚举器的Dispose方法,这样可以让这个枚举器变成可重用状态。

总结

Unity系统的泛型List存在的问题是:它在finally中回收枚举器时执行了Box操作。自定义List时,正确实现泛型格式的IEnumerable、IEnumerator是关键,需要避开枚举单元被Current时,值类型被强制转换成对象类型的Box操作。

总体上来说,这应该是一个高效的,无GC的List,看官可以尝试一哈。

【Unity优化】构建一个拒绝GC的List的更多相关文章

  1. Unity优化之GC——合理优化Unity的GC

      转载请标明出处http://www.cnblogs.com/zblade/ 最近有点繁忙,白天干活晚上抽空写点翻译,还要运动,所以翻译工作进行的有点缓慢 =.= PS: 最近重新回来更新了一遍,文 ...

  2. EasyAR SDK在unity中的简单配置及构建一个简单场景。

    首先打开EasyAR的官方网站http://www.easyar.cn/index.html,注册登陆之后,打开首页的开发页面. 下载sdk和Unity Samples. 创建一个unity3d工程N ...

  3. Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践

    protobuf-net优化效果图 protobuf-net是Unity3D游戏开发中被广泛使用的Google Protocol Buffer库的c#版本,之所以c#版本被广泛使用,是因为c++版本的 ...

  4. [Unity优化] Unity CPU性能优化

    前段时间本人转战unity手游,由于作者(Chwen)之前参与端游开发,有些端游的经验可以直接移植到手游,比如项目框架架构.代码设计.部分性能分析,而对于移动终端而言,CPU.内存.显卡甚至电池等硬件 ...

  5. 【Unity优化】Unity优化技巧进阶开篇

    版权声明:本文为博主原创文章,欢迎转载.请保留博主链接:http://blog.csdn.net/andrewfan 做游戏好多年了,关于游戏优化一直是令开发者头疼的一个问题.因为优化牵扯的内容很多, ...

  6. 再议Unity优化

    0x00 前言 在很长一段时间里,Unity项目的开发者的优化指南上基本都会有一条关于使用GetCompnent方法获取组件的条目(例如14年我的这篇博客<深入浅出聊Unity3D项目优化:从D ...

  7. Unity优化方向——优化Unity游戏中的图形渲染(译)

    CPU bound:CPU性能边界,是指CPU计算时一直处于占用率很高的情况. GPU bound:GPU性能边界,同样的是指GPU计算时一直处于占用率很高的情况. 原文:https://unity3 ...

  8. Unity优化方向——优化Unity游戏中的脚本(译)

    原文地址:https://unity3d.com/cn/learn/tutorials/topics/performance-optimization/optimizing-scripts-unity ...

  9. (转载)Unity 优化总结

    Unity 优化总结 2017-03-10 | 发布 大海明月  zengfeng75@qq.com  | 分类 Unity  | 标签 Unity  优化 相关文档 UGUI 降低填充率技巧两则 U ...

随机推荐

  1. js模块化开发——模块的写法

    随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码越来越庞大,越来越复杂. 网页越来越像桌面程序,需要一个团队分工协作.进度管理.单元测试等等......开发者 ...

  2. BNU Online Judge-29140

    题目链接 http://www.bnuoj.com/bnuoj/problem_show.php?pid=29140 看到这样的题,应该想到an  与an-1  的关系,他们是会有关系的你要去找 代码 ...

  3. 【原】小写了一个cnode的小程序

    小程序刚出来的第一天,朋友圈被刷屏了,所以趁周末也小玩了一下小程序.其实发觉搭建一个小程序不难,只要给你一个demo,然后自己不断的查看文档,基本就可以入门了,不过对于这种刚出来的东西,还是挺多坑的, ...

  4. Linux笔记(十三) - 系统管理

    (1)进程管理1.判断服务器健康状态2.查看进程a.查看系统中所有进程:ps    例:ps aux(使用BSD操作系统命令格式)    a 显示所有前台进程    x 显示所有后台进程    u 显 ...

  5. 我的Linux软件集

    把我常用的软件记下来,以后重装安装的时候方便一些- 这个博文会不断更新的- 开发工具类 Monodevelop 编写C#控制台程序和GTK#窗口程序,很好,虽然没有VS强大,但是够用了 CodeBlo ...

  6. Dev的WPF控件与VS2012不兼容问题

    在只有vs2010环境下Dev的wpf可以在视图模式下显示,但是安装vs2012后无法打开界面的视图模式,报错:无法创建控件实例! 发现是Dev的wpf控件与.net framework 4.5不兼容 ...

  7. 微端启动器LAUNCHER的制作之MFC版二(下载)

    用了C#再回来用C++写真的有一种我已经不属于这个世界的感觉.C++的下载就没有C#那么方便了,我用的是libcurl.dll,官网上下载的源码自己cmake出来编译的,c++的库引用有debug和r ...

  8. 性能秒杀log4net的NLogger日志组件(附测试代码与NLogger源码)

    NLogger特性: 一:不依赖于第三方插件和支持.net2.0 二:支持多线程高并发 三:读写双缓冲对列 四:自定义日志缓冲大小 五:支持即时触发刷盘机制 六:先按日期再按文件大小滚动Rolling ...

  9. linux系统盘使用率达到100%的问题查找和解决方法

    今天公司云服务器报警系统发来短信,系统磁盘空间不够,登录服务器进行查看,磁盘使用虑达到100%,       感觉比较奇怪,所存的东西并不多,怎么会将磁盘占满,而且数据都是存在数据盘下,通过简单的进行 ...

  10. 钉钉开发笔记(6)使用Google浏览器做真机页面调试

    注: 参考文献:https://developers.google.com/web/ 部分字段为翻译文献,水平有限,如有错误敬请指正 步骤1: 从Windows,Mac或Linux计算机远程调试And ...