前断时间做项目的时候,发现一个问题:动态导入的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特性并没有起作用。
至此,本篇文章就到尾声了,或许自己做的这个小功能在实际开发中根本碰不上也用不上,但是还是可以给我们一些启发:在产品设计或者框架设计中,可以适当的存储一些关键信息,用些许的内存,换来效率的大提升,使用者在用起来也非常简便,好接受,并且有较好的包容性,用起来不易出错。