02 ILRT的基础使用

加载ILRuntime

public class HelloWorld : MonoBehaviour
{
    //AppDomain是ILRuntime的入口,最好是在一个单例类中保存,整个游戏全局就一个,这里为了示例方便,每个例子里面都单独做了一个
    //大家在正式项目中请全局只创建一个AppDomain
    AppDomain appdomain;

    System.IO.MemoryStream fs;
    System.IO.MemoryStream p;
    void Start()
    {
        StartCoroutine(LoadHotFixAssembly());
    }

    IEnumerator LoadHotFixAssembly()
    {
        //首先实例化ILRuntime的AppDomain,AppDomain是一个应用程序域,每个AppDomain都是一个独立的沙盒
        appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
        
        //正常项目中应该是自行从其他地方下载dll,或者打包在AssetBundle中读取,平时开发以及为了演示方便直接从StreammingAssets中读取,
        //正式发布的时候需要大家自行从其他地方读取dll
        var requeset = UnityWebRequest.Get(new Uri(Path.Combine(Application.streamingAssetsPath, "HotFix_Project.dll")));
        requeset.SendWebRequest();
        while (!requeset.isDone)
            yield return null;
        if (!string.IsNullOrEmpty(requeset.error))
            UnityEngine.Debug.LogError(requeset.error);
        byte[] dll = requeset.downloadHandler.data;

        //PDB文件是调试数据库,如需要在日志中显示报错的行号,则必须提供PDB文件,不过由于会额外耗用内存,正式发布时请将PDB去掉,下面LoadAssembly的时候pdb传null即可
        requeset = UnityWebRequest.Get(new Uri(Path.Combine(Application.streamingAssetsPath, "HotFix_Project.pdb")));
        requeset.SendWebRequest();
        while (!requeset.isDone)
            yield return null;
        if (!string.IsNullOrEmpty(requeset.error))
            UnityEngine.Debug.LogError(requeset.error);
        byte[] pdb = requeset.downloadHandler.data;
        
        fs = new MemoryStream(dll);
        p = new MemoryStream(pdb);
        try
        {
            // 正式版本时不传入PDB文件,置空
            appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
        }
        catch
        {
            Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
        }

        InitializeILRuntime();
        OnHotFixLoaded();
    }

    void InitializeILRuntime()
    {
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
        //由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
        appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
        //这里做一些ILRuntime的注册,HelloWorld示例暂时没有需要注册的
    }

    void OnHotFixLoaded()
    {
        //HelloWorld,第一次方法调用
        appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);

    }

    private void OnDestroy()
    {
        if (fs != null)
            fs.Close();
        if (p != null)
            p.Close();
        fs = null;
        p = null;
    }
}

官网DEMO里加载步骤已经写的非常清楚了,不过DEMO中原本使用的是WWW加载,2018以后还是建议永UnityWebRequest,在这里我已经替换成WebRequest了

调用热更中的方法

Invoke(类名,方法名,类型实例,参数列表)

当调用的是静态方法时,参数3可以传入NULL,非静态方法那就必须传入一个类型实例了,和反射基本一样

//调用无参数静态方法,appdomain.Invoke("类名", "方法名", 对象引用, 参数列表);
appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null);
//调用带参数的静态方法
appdomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest2", null, 123);

如果方法会被频繁调用,那每次都类似于发射查找,肯定是有性能消耗的,此时可以把方法“缓存”起来

//预先获得IMethod,可以减低每次调用查找方法耗用的时间
IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
//根据方法名称和参数个数获取方法
IMethod method = type.GetMethod("StaticFunTest2", 1);
appdomain.Invoke(method, null, 123);

当然也可以根据泛型和参数列表获取方法

//参数类型列表
IType intType = appdomain.GetType(typeof(int));
List<IType> paramList = new List<ILRuntime.CLR.TypeSystem.IType>();
paramList.Add(intType);
//根据方法名称和参数类型列表获取方法,第二个参数是泛型
method = type.GetMethod("StaticFunTest2", paramList, null);
appdomain.Invoke(method, null, 456);

但是在ILRT中调用方法时,参数是以参数列表的形式传入的params object[],那不可避免的会产生内存分配问题,如果方法需要被频繁调用,可以这么做

using (var ctx = appdomain.BeginInvoke(method))
{
    // 入栈顺序就是参数顺序
	ctx.PushInteger(123);
	ctx.Invoke();
}

比较特殊的C#中的属性也可以通过方法调用,因为基本上属性就会被编译成一个方法

比如

public int ID{get;set}
method = type.GetMethod("get_ID", 0);
using (var ctx = appdomain.BeginInvoke(method))
{
	ctx.PushObject(obj);
    ctx.Invoke();
	int id = ctx.ReadInteger();
}

get_ID是C#编译后的默认名称,传入的obj则是类型实例,最后我们可以通过ReadInterger()读出结果

当然也会有方法set_ID

最后,最特殊的类型,带有ref和out的方法

public void RefOutMethod(int addition, out List<int> lst, ref int val)
{
	val = val + addition + id;
	lst = new List<int>();
	lst.Add(id);
}
method = type.GetMethod("RefOutMethod", 3);
int initialVal = 500;
using(var ctx = appdomain.BeginInvoke(method))
{
    //第一个ref/out参数初始值
    ctx.PushObject(null);
    //第二个ref/out参数初始值
    ctx.PushInteger(initialVal);
    //压入this
    ctx.PushObject(obj);
    //压入参数1:addition
    ctx.PushInteger(100);
    //压入参数2: lst,由于是ref/out,需要压引用,这里是引用0号位,也就是第一个PushObject的位置
    ctx.PushReference(0);
    //压入参数3,val,同ref/out
    ctx.PushReference(1);
    ctx.Invoke();
    //读取0号位的值
    List<int> lst = ctx.ReadObject<List<int>>(0);
    initialVal = ctx.ReadInteger(1);

    Debug.Log(string.Format("lst[0]={0}, initialVal={1}", lst[0], initialVal));
}

实例化热更类

object obj = appdomain.Instantiate("HotFix_Project.InstanceClass", new object[] { 233 });

或者

IType type = appdomain.LoadedTypes["HotFix_Project.InstanceClass"];
// 默认会调用无参构造方法,有参数的话也可以直接指定
object obj2 = ((ILType)type).Instantiate();
上一篇:编写自己的handler简单操作redis


下一篇:编号226:翻转二叉树