unity 动态导入dll与RuntimeInitializeOnLoadMethod的实现

前断时间做项目的时候,发现一个问题:动态导入的dll(即打包成exe后再放入包中的dll),unity提供的特性RuntimeInitializeOnLoadMethod标注的方法没有被调用。

代码很简单,只是测试用,如下:

namespace TestProject
{
    public class Test
    {
        [RuntimeInitializeOnLoadMethod]
        private static void Init()
        {
            Debug.Log("Test.Init func");
        }
    }
}

Test的Init函数加上RuntimeInitializeOnLoadMethod和去掉RuntimeInitializeOnLoadMethod后分别出包,对比后发现,只有globalgamemanagers文件有不同,虽然该文件已经加密,但用BCompare打开后我们可以看到些许端倪,见下图蓝色框框处:

原来unity把有用RuntimeInitializeOnLoadMethod的相关信息存储在了该文件中,TestProject为dll名称,TestProject.Test为命名空间,Init为使用到特性的函数。

这是一个聪明的做法,大家都知道C#的反射是很耗费时间的,如果不把上面的信息提前存储起来,而等到运行时再加载所有的dll,得到所有的Type,一个一个的函数查询有没有用到RuntimeInitializeOnLoadMethod的话,那效率肯定是非常感人的。所以Unity把这部分时间分摊到了编辑期,每把一个dll考入unity工程或者有改变dll时,Unity都会对其进行分析,然后把感兴趣的内容存储起来,运行时根据这些内容,精确的找到相应的点,执行就好了(笔者自行yy的)。

那具体是怎么做的呢?其实很简单,我们一起来实现这个功能:

就是用到了一些c#反射的知识,获取RuntimeInitializeOnLoadMethod实现思路是这样的:在某个时机点,获取出所有感兴趣的dll--》依次加载dll--》得到dll中所有的Type--》获取这个Type下所有的Static函数--》判断该static函数是否有用到RuntimeInitializeOnLoadMethod特性--》有则记录相关信息。

需要用到几个关键函数为:Assembly.load、Assembly.GetType 、Type.GetMethods 、MemberInfo.GetCustomAttributes。

需要获取哪些信息呢:dll名称、类型全称(命名空间+类名)、函数名称、函数的参数个数(为什么要有这个后文会说到),因为一个类有可能有多个函数有用到这个关键字,所以我们可以这样设计结构:

public class DllAndTypeInfo
        {
            public DllAndTypeInfo(string strAssembly, string strNameSpace)
            {
                this.strAssembly = strAssembly;
                this.strFullName = strNameSpace;
            }

            public string strAssembly{ get; set; }

            public string strFullName{ get; set; }
        }

        public class FuncInfo
        {
            public FuncInfo(int nParamCount, string strName)
            {
                this.nParamCount = nParamCount;
                this.strName = strName;
            }

            public int nParamCount{ get; set; }

            public string strName{ get; set; }
        }

        Dictionary<DllAndTypeInfo, List<FuncInfo>> dicAttributeInfo = new Dictionary<DllAndTypeInfo, List<FuncInfo>>();

以dll名 + 类型全称作为key,value为该类使用RuntimeInitializeOnLoadMethod关键字的函数信息集合。

定义好了数据结构,我们就可以开始采集信息了,就以前面的测试代码为例:

        public void OnlyTest()
        {
            this.GetAttributeInfo("TestProject");
        }

        public void GetAttributeInfo(string strAssemblyName)
        {
            Assembly objAssembly = Assembly.Load(strAssemblyName);
            if (null == objAssembly)
            {
                return;
            }

            foreach (var type in objAssembly.GetTypes())
            {
                if (null == type)
                {
                    continue;
                }

                List<FuncInfo> lstFuncInfo = new List<FuncInfo>();
                MethodInfo[] arrMemberinfo = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
                foreach (var memberinfo in arrMemberinfo)
                {
                    object[] arrAttributes = memberinfo.GetCustomAttributes(typeof(RuntimeInitializeOnLoadMethodAttribute), true);
                    if (arrAttributes.Length <= 0)
                    {
                        continue;
                    }

                    FuncInfo objFuncInfo = new FuncInfo(memberinfo.GetParameters().Length, memberinfo.Name);
                    lstFuncInfo.Add(objFuncInfo);
                }

                if (lstFuncInfo.Count <= 0)
                {
                    return;
                }

                DllAndTypeInfo objAssemblyNs = new DllAndTypeInfo(objAssembly.GetName().Name, type.FullName);
                dicAttributeInfo[objAssemblyNs] = lstFuncInfo;
            }
        }

信息采集好了,我们就可以选个时机使用它了,调用很简单,除了前面用到的Assembly.load、Assembly.GetType 外,用的是Type.InvokeMember来调用函数,这边来解释下前文为什么要记录参数个数,因为Type.InvokeMember必须传入参数数组,如果参数数组个数不匹配将报错,无法执行。使用unity测试发现,不管参数个数多少,unity都能正确的调用有标注该特性的函数(当然,入参的值是无意义的),所以我们也要做此操作。

还有个问题是,为什么RuntimeInitializeOnLoadMethod要规定标注该特性的函数必须为static函数?原因是:虽然在调用Type.InvokeMember的时候可以指定类型的实例,但是该实例在反射的时候无从获取,如果自行创建一个,那肯定不是用户想要的,况且也不知道指针赋值给谁,函数执行完毕下次GC就回收了,类如果是私有的话,还无法创建类实例,总之不是static的话,会有各种各样的问题。

 

疑问解决了,我们接着往下,调用的代码为:

        public void CallAttributeFunc()
        {
            foreach (var item in dicAttributeInfo)
            {
                Assembly objAssembly = Assembly.Load(item.Key.strAssembly);
                if (null == objAssembly)
                {
                    continue;
                }

                Type objType = objAssembly.GetType(item.Key.strFullName);
                if (null == objType)
                {
                    continue; ;
                }

                foreach (var method in item.Value)
                {
                    object[] objArgs = method.nParamCount > 0 ? new object[method.nParamCount] : null;
                    objType.InvokeMember(method.strName, BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Static, null, null, objArgs);
                }
            }
        }

剩下的就是获取和调用的入口了,来个简单的测试:

        private void OnGUI()
        {
            if (GUI.Button(new Rect(100, 100, 100, 100), "加载"))
            {
                this.OnlyTest();
            }

            if (GUI.Button(new Rect(100, 300, 100, 100), "调用"))
            {
                this.CallAttributeFunc();
            }
        }

来看看演示的gif:

下图为TestProject.dll放入工程中一起出包的效果:

可以看到打印了两次Test.Init func,一次是因为RuntimeInitializeOnLoadMethod特性起作用,unity进行调用,一次是我们自己加载后进行调用。

下图为把TestProject.dll从工程中删除,打完exe包后,再放入到打出包的***_Data/Managed文件夹中:

这次只打印了一次的Test.Init func,RuntimeInitializeOnLoadMethod特性并没有起作用。

 

至此,本篇文章就到尾声了,或许自己做的这个小功能在实际开发中根本碰不上也用不上,但是还是可以给我们一些启发:在产品设计或者框架设计中,可以适当的存储一些关键信息,用些许的内存,换来效率的大提升,使用者在用起来也非常简便,好接受,并且有较好的包容性,用起来不易出错。


版权声明:本文为fdyshlk原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。