(1)程序集的管理
1.创建一个新的文件夹(之所以不在Assets根目录下直接创建程序集,因为根目录下创建的程序集会取代Assembly-CSharp.dll)
2.通过 Assets > Create > Assembly Definition,也可以直接通过右键菜单创建。
- 每个文件夹只能创建一个“Assembly Definition”或“Assembly Definition Reference”。
- Use GUID 选项
与新建的程序集处于同一层级或者处于子层级的所有脚本都编译到该程序集。
[InitializeOnLoad]
public class Startup
{
private const string HotfixDll1 = "Library/ScriptAssemblies/PlatformHotfix.dll";
private const string HotfixPdb1 = "Library/ScriptAssemblies/PlatformHotfix.pdb";
//生成路径
public static string IL_HotFixFile_Dll1 = "Assets/LoadResources/Dll/HotFix.dll.bytes";
public static string IL_HotFixFile_PDB1 = "Assets/LoadResources/Dll/HotFix.pdb.bytes";
static Startup()
{
CopyDll();
}
public static bool CompareFile(string filePath1, string filePath2)
{
if (!File.Exists(filePath1) || !File.Exists(filePath2))
{
return false;
}
//计算第一个文件的哈希值
HashAlgorithm hash = HashAlgorithm.Create();
var stream_1 = new System.IO.FileStream(filePath1, System.IO.FileMode.Open);
byte[] hashByte_1 = hash.ComputeHash(stream_1);
stream_1.Close();
//计算第二个文件的哈希值
var stream_2 = new System.IO.FileStream(filePath2, System.IO.FileMode.Open);
byte[] hashByte_2 = hash.ComputeHash(stream_2);
stream_2.Close();
return BitConverter.ToString(hashByte_1) == BitConverter.ToString(hashByte_2);
}
public static void CopyDll()
{
var folder = Path.GetDirectoryName(IL_HotFixFile_Dll1);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
if (!CompareFile(HotfixDll1, IL_HotFixFile_Dll1))
{
if (File.Exists(HotfixDll1))
{
File.Copy(HotfixDll1, IL_HotFixFile_Dll1, true);
File.Copy(HotfixPdb1, IL_HotFixFile_PDB1, true);
Debug.Log($"成功复制 Hotfix.dll, Hotfix.pdb到 目录 " + folder);
}
}
}
}
(2)程序集的加载
private async GVoid LoadHotFix()
{
var dllAsset = await ResourceMgr.Instance.LoadAssetAsync<TextAsset>("Dll/HotFix.dll");
byte[] assBytes = (dllAsset.result as TextAsset).bytes;
var pdbAsset = await ResourceMgr.Instance.LoadAssetAsync<TextAsset>("Dll/HotFix.pdb");
byte[] pdbBytes = (pdbAsset.result as TextAsset).bytes;
#if ILRuntime
Debug.Log($"当前使用的是ILRuntime模式");
this.appDomain = new ILRuntime.Runtime.Enviorment.AppDomain();
this.dllStream = new MemoryStream(assBytes);
this.pdbStream = new MemoryStream(pdbBytes);
this.appDomain.LoadAssembly(this.dllStream, this.pdbStream, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
#if UNITY_EDITOR
this.appDomain.DebugService.StartDebugService(56000);
#endif
ILHelper.InitILRuntime(this.appDomain);
hotfixTypes = this.appDomain.LoadedTypes.Values.Select(x => x.ReflectionType).ToList();
Platform.ILBridge.Init(CreateInstance, hotfixTypes);
//await GAsync.WaitSeconds(10);
var iMethod = appDomain.GetType("PlatformHotfix.Init").GetMethod("Awake", 0);
this.appDomain.Invoke(iMethod, null);
#else
Debug.Log($"当前使用的是Mono模式");
this.assembly = Assembly.Load(assBytes, pdbBytes);
this.hotfixTypes = this.assembly.GetTypes().ToList();
Platform.ILBridge.Init(CreateInstance, hotfixTypes);
var methodInfo = assembly.GetType("PlatformHotfix.Init").GetMethod("Awake");
methodInfo?.Invoke(null, new object[methodInfo.GetParameters().Length]);
#endif
}
Mono模式是为了开发时看堆栈断点方便。
ILLauncher游戏的入口,ILHelper所有适配器添加
(3)生成跨域继承适配器
[MenuItem("ILRuntime/生成跨域继承适配器")]
static void GenerateCrossbindAdapter()
{
//由于跨域继承特殊性太多,自动生成无法实现完全无副作用生成,所以这里提供的代码自动生成主要是给大家生成个初始模版,简化大家的工作
//大多数情况直接使用自动生成的模版即可,如果遇到问题可以手动去修改生成后的文件,因此这里需要大家自行处理是否覆盖的问题
using(System.IO.StreamWriter sw = new System.IO.StreamWriter("Assets/Scripts/Main/ILRuntime/ILAdaptor/IEnumeratorContainerAdaptor.cs"))
{
//sw.WriteLine(ILRuntime.Runtime.Enviorment.CrossBindingCodeGenerator.GenerateCrossBindingAdapterCode(typeof(View), "GalaSportILRuntime"));
//sw.WriteLine(ILRuntime.Runtime.Enviorment.CrossBindingCodeGenerator.GenerateCrossBindingAdapterCode(typeof(Controller), "GalaSportILRuntime"));
sw.WriteLine(ILRuntime.Runtime.Enviorment.CrossBindingCodeGenerator.GenerateCrossBindingAdapterCode(typeof(System.Collections.IEnumerator), "GalaSportILRuntime"));
}
AssetDatabase.Refresh();
}
(4)自动分析热更DLL生成CLR绑定
[MenuItem("ILRuntime/通过自动分析热更DLL生成CLR绑定")]
static void GenerateCLRBindingByAnalysis()
{
//用新的分析热更dll调用引用来生成绑定代码
ILRuntime.Runtime.Enviorment.AppDomain domain = new ILRuntime.Runtime.Enviorment.AppDomain();
using (System.IO.FileStream fs = new System.IO.FileStream("Assets/LoadResources/Dll/HotFix.dll.bytes", System.IO.FileMode.Open, System.IO.FileAccess.Read))
{
domain.LoadAssembly(fs);
//Crossbind Adapter is needed to generate the correct binding code
InitILRuntime(domain);
ILRuntime.Runtime.CLRBinding.BindingCodeGenerator.GenerateBindingCode(domain, "Assets/Scripts/Main/ILRuntime/Generated");
}
AssetDatabase.Refresh();
}
产生的代码:
通常情况下,如果要从热更DLL中调用Unity主工程或者Unity的接口,是需要通过反射接口来调用的,包括市面上不少其他热更方案,也是通过这种方式来对CLR方接口进行调用的。
但是这种方式有着明显的弊端,最突出的一点就是通过反射来调用接口调用效率会比直接调用低很多,再加上反射传递函数参数时需要使用object[]
数组,这样不可避免的每次调用都会产生不少GC Alloc。众所周知GC Alloc高意味着在Unity中执行会存在较大的性能问题。
ILRuntime通过CLR方法绑定机制,可以选择性
的对经常使用的CLR接口进行直接调用,从而尽可能的消除反射调用开销以及额外的GC Alloc
CLR说明:
CLR意思是公共语言运行库和Java虚拟机一样也是一个运行时的环境,它负责资源管理(内存分配和垃圾收集等),并保证应用和底层操作系统之间必要的分离。CLR存在两种不同的翻译名称:公共语言运行库和公共语言运行时。CLR是Common Language Runtime的简写。
CLR和Java虚拟机一样也是一个运行时环境,是一个可由多种编程语言使用的运行环境。CLR的核心功能包括:内存管理、程序集加载、安全性、异常处理和线程同步,可由面向CLR的所有语言使用。并保证应用和底层操作系统之间必要的分离。CLR是.NET Framework的主要执行引擎。
扩展资料
1.为了提高平台的可靠性,以及为了达到面向事务的电子商务应用所要求的稳定性级别,CLR还要负责其他一些任务,比如监视程序的运行。按照.NET的说法,在CLR监视之下运行的程序属于“托管的”代码,而不在CLR之下、直接在裸机上运行的应用或者组件属于“非托管的”的代码。
2.CLR将监视形形色色的常见编程错误,许多年来这些错误一直是软件故障的主要根源,其中包括:访问数组元素越界,访问未分配的内存空间,由于数据体积过大而导致的内存溢出,等等。
(5)例子
反射,协程,继承,值类型,CLR绑定
(6)优化
1,
2,
3
一定要特别注意,:后面只允许有1个Unity主工程的类或者接口,但是可以有随便多少个热更DLL中的接口
4,
注释或者解注InitializeILRuntime里的代码来对比进行值类型绑定前后的性能差别
5,
IL的虚拟机是一个堆栈式结构的机制
- Managed Heap(托管堆):这就是NET中的托管堆,用来存放引用类型,它是由GC(垃圾回收器自动进行回收)管理;比如声明字符串str = "123",就是将"123"保存在托管堆,然后要用的时候再将"123"的地址给计算栈
- Call Stack(调用堆栈):调用堆栈是一个方法列表,按调用顺序保存所有在运行期被调用的方法。
- Evaluation Stack(计算堆栈):每个线程都有自己的线程栈,IL 里面的任何计算,都发生在计算栈上。可以 Push,也可以 Pop。比如计算1 + "str",就是先将数字1入栈,然后再将1封箱为"1"替换,再将"str"的引用地址入栈,然后调用合并函数将栈顶前两个字符串连接起来,再将结果的引用放在栈顶。
优化 :
ILRuntime在进行方法调用时,需要将方法的参数先压入托管栈,然后执行完毕后需要将栈还原,并把方法返回值压入栈。
具体过程如下图所示
|
函数调用进入目标方法体后,栈指针(后面我们简称为ESP)会被指向方法栈基址那个位置,可以通过ESP-X获取到该方法的参数和方法内部申明的局部变量,在方法执行完毕后,如果有返回值,则把返回值写在方法栈基址位置即可(上图因为空间原因写在了基址后面)。
当方法体执行完毕后,ILRuntime会自动平衡托管栈,释放所有方法体占用的栈内存,然后把返回值复制到参数1的位置,这样后续代码直接取栈顶部就可以取到上次方法调用的返回值了。
参考:
[1]http://ourpalm.github.io/ILRuntime/public/v1/guide/reflection.html