众所周知当我们写匿名函数并引用局部变量的时候会产生GC,这是因为在调用局部变量时,Unity需要将这个局部变量缓存起来,以调用,所以就会额外生成一个类,这样就会产生额外的GC。
具体我们可以看这里的测试案例。

    static int s_a = 1;
    int m_a = 1;
    public void Func(Action action)
    {
        action();
    }
    public void Update()
    {
        int a = 1;
        Profiler.BeginSample("1");
        Func(() => a = 2);
        Profiler.EndSample();

        Profiler.BeginSample("2");
        Func(() => s_a = 2);
        Profiler.EndSample();

        Profiler.BeginSample("3");
        Func(() => m_a = 2);
        Profiler.EndSample();

        Profiler.BeginSample("4");
        Func(() => { int b = 2; });
        Profiler.EndSample();
    }

可以看到 调用 a 和 m_a 的时候都产生了GC,那么a产生的原因我们已经直到了,为什么m_a还会产生呢,他明明是全局变量啊!
{
这是因为匿名函数没法把他包装成静态函数,只能每帧动态创建一个action所以也会产生额外的GC,我们可以稍微做下修改来消除这个GC。

    Action action;
    int m_a = 1;
    public void Func(Action action)
    {
        action();
    }
    private void Start()
        action = actionTest;
    }
    public void Update()
    {
        Profiler.BeginSample("action");
        Func(action);
        Profiler.EndSample();
    }


这样这个GC也就被完美解决了,这时候我就在想我使用匿名函数的时候,大都是使用委托并且有些变量只能延迟创建的情况才会去使用匿名函数,那么我到底有没有办法把他们拆出来干掉这部分GC呢?

//测试类 用来充当UI中常见种类Button
public class Button
{
    public Action action;
    public void Add(Action action)
    {
        this.action += action;
    }
}
//直接执行类 就是我拆出来的那个类
public class Test
{
    public System.Object testObject = new System.Object();
    public Test() { }
    public Test(System.Object testObject)
    {
        this.testObject = testObject;
    }
    public void action() { }
}
public class Holographic : MonoBehaviour
{
    Action action;
    List<Button> buttons =new List<Button>() { new Button(),new Button()};
    Test test = new Test();
    void Update()
    {
        foreach (var item in buttons)
        {
            System.Object testObject = new System.Object();
            test.testObject = testObject;
            action = test.action;

            Profiler.BeginSample("Use Class");
            item.Add(action);
            Profiler.EndSample();

            Profiler.BeginSample("Lambda");
            item.Add(() => { actionObject(testObject); });
            Profiler.EndSample();
        }
    }
    public void actionObject(System.Object testObject) { }
}


可以看到我们明明已经拆出来了却还是产生了额外的GC这是为什么呢?
一开始我也不能理解,后来我把他翻译成了IL代码

发现明明没有new的操作啊为什么还是有GC???
后再我在翻阅了CLR和C#本质论的书以及微软关于委托的源码后才找到了问题的关键。

C#的Delegate关键字,继承自MulticastDelegate,包括action或者是自己声明的委托都是从MulticastDelegate继承下来的 ,MulticastDelegate内部自己维护了一个链表,用来做多播委托,这就是委托链,+=是会调用重写的CombineImpl方法返回一个新的委托从而产生了GC。

这部分我们在CLR书中也可以看到

这里是微软的源码,并不是很全,感兴趣的可以自己去详细研究,这里就不深究了。

// This method will combine this delegate with the passed delegate
        //    to form a new delegate.
        [System.Security.SecuritySafeCritical]  // auto-generated
        protected override sealed Delegate CombineImpl(Delegate follow)
        {
            if ((Object)follow == null) // cast to object for a more efficient test
                return this;

            // Verify that the types are the same...
            if (!InternalEqualTypes(this, follow))
                throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTypeMis"));

            MulticastDelegate dFollow = (MulticastDelegate)follow;
            Object[] resultList;
            int followCount = 1;
            Object[] followList = dFollow._invocationList as Object[];
            if (followList != null)
                followCount = (int)dFollow._invocationCount; 

            int resultCount;
            Object[] invocationList = _invocationList as Object[];
            if (invocationList == null)
            {
                resultCount = 1 + followCount;
                resultList = new Object[resultCount];
                resultList[0] = this;
                if (followList == null)
                {
                    resultList[1] = dFollow;
                }
                else
                {
                    for (int i = 0; i < followCount; i++)
                        resultList[1 + i] = followList[i];
                }
                return NewMulticastDelegate(resultList, resultCount);
            }
            else
            {
                int invocationCount = (int)_invocationCount;
                resultCount = invocationCount + followCount;
                resultList = null;
                if (resultCount <= invocationList.Length)
                {
                    resultList = invocationList;
                    if (followList == null)
                    {
                        if (!TrySetSlot(resultList, invocationCount, dFollow))
                            resultList = null;
                    }
                    else
                    {
                        for (int i = 0; i < followCount; i++)
                        {
                            if (!TrySetSlot(resultList, invocationCount + i, followList[i]))
                            {
                                resultList = null;
                                break;
                            }
                        }
                    }
                }

                if (resultList == null)
                {
                    int allocCount = invocationList.Length;
                    while (allocCount < resultCount)
                        allocCount *= 2;

                    resultList = new Object[allocCount];

                    for (int i = 0; i < invocationCount; i++)
                        resultList[i] = invocationList[i];

                    if (followList == null)
                    {
                        resultList[invocationCount] = dFollow;
                    }
                    else
                    {
                        for (int i = 0; i < followCount; i++)
                            resultList[invocationCount + i] = followList[i];
                    }
                }
                return NewMulticastDelegate(resultList, resultCount, true);
            }
        }

最后还有个有意思的点,就是如果我们的委托一开始是空的那么是不会产生GC。

我们可以看到如果a为空直接就返回b,也就是你传了+=了个啥,他还是会把那玩意原封不动的还给你。

看到这里也就懂了,如果条件允许的情况下,我们可以不使用微软给我们的委托链,而是我们自己封装一套0GC的委托链,来实现多播委托。
这里给上来自Game+群,夜莺大佬提供的一个解决方案https://github.com/SixWays/Relay

注意!
我这里仅仅只是对+=做了分析,我在测试中发现
test.testObject = testObject;
action = test.action;
这里的代码还是会产生对应GC的!我个人并没有什么好的解决方案,希望大佬能提供思路。


学无止境,知行合一,学以致用,举一反三。