HybridCLR(代号wolong)/huatuo新一代热更新方案

前言

huatuo 现已改名 HybridCLR, 而原来的huatuo仓库由途游接着维护

本文章主要以HybridCLR仓库为例说明:focus creative games(代码哲学) · GitHub

 hybridclr  仓库为核心仓库
 il2cpp_plus 仓库为改造过的il2cpp仓库添加解释核心仓库的代码指令
 hybridclr_trial 仓库为示例仓库

Unity IL2CPP 技术原理与AOT

在此之前,我们先了解下Unity的发展:

  • 阶段一: C#的出现;
            微软开发出一套符合自己的标准出来,于是在Windows系列的平台上把.net的标准开发出来,就是说我们开发者只需要开发出来符合.net 字节码的标准代码, 我们就可以让我们的代码在.net上运行起来;
  • 阶段二: Mono项目; 
            为了实现跨平台(Linux, Windows, Android,MacOS等平台), 于是就有了.net虚拟机的出现,结合.net开发环境,造就了Mono项目;
  • 阶段三: Unity采用了Mono方案实现了跨平台
  • 阶段四: IL2CPP 项目;
            为什么会出现IL2CPP 项目, Unity发现使用Mono出现了那些问题?
    a: 程序的执行效率: 编写出来的.net字节码是基于.net虚拟机上运行,再编译成二进制机器指令;
    b: 针对不同的平台,Unity就要把VM都给移植一遍,同时解决VM里面发现的bug,这非常耗时耗力。而且有些平台无法进行移植;
    c: 因为Mono的授权受限,Unity无法升级Mono版本导致一些新的C#特性无法使用;

综上所了解, Unity独自开发出了一条新技术路线就是所谓的 IL2CPP,根据官方的实验数据,换成IL2CPP以后,程序的运行效率有了1.5-2.0倍的提升;

详细介绍参考: Unity将来时:IL2CPP是什么? - 知乎

这里主要说两个知识点:

  1. 什么是IL?
    IL(Intermediage Language) 中间语言,CLR把C#,java,F#等语言编写的代码都统编译成IL,这样就可以在IL 这一层实现 所有编程语言的统一,vs将编译后的IL存储在程序集中(Dll\exe)l文件中,当打开软件的时候 CLR程序集中的IL 二次编译成机器可识别的语言。
    简单地说:在Unity中,IL和CIL表示的是同一个东西,它是一种属于通用语言架构和.net框架的低阶的编程语言
  2. 什么是CLI?
    CLI(Common Language Infrastructure)通用语言基础架构,CLI是一个开放型的技术规范,它定义了一个语言无关的跨体系结构的运行环境,这使得开发者可以用规范内定义的各种高级语言来开发软件,并且无需修正即可将软件运行在不同的计算机体系结构上.由微软、惠普和英特尔于2000年向ECMA倡议的。最终定义为Ecma335标准。
    简单地说: 特指在.net平台下的IL标准

这时候肯定说这个有什么关联吗?.net是一个程序开发、运行平台,它是CLI的实现,它是要实现让程序源跨语言开发软件、让开发出来的软件跨平台 、跨系统运行。 我画张图就懂其中的道道了

 所以说,.net帮我们把在Unity中开发的C#代码转为符合IL字节码就可以在.net上运行;

那么IL2CPP与上有什么区别呢?
IL2CPP在得到中间语言IL字节码后,使用IL2CPP将他们重新变为静态的编译型语言(C语言,C++语言)C++代码,然后再由各个平台(xcode,android ndk, windows visual studio等)的C++编译器来编译我们的代码,把我们的代码编程本地的机器指令,直接在os上执行,那么效率上来看肯定比在.net解释执行嘎嘎的快了;

IL2CPP只支持AOT方式,Mono支持AOT,JIT所有方式;
Android平台支持Mono和IL2CPP的所有编译方式;
IOS只支持Mono的Full AOT模式和IL2CPP;

那么什么是AOT呢?
AOT: Ahead of time 离线编译
就是把我们的高级语言➡静态编译型语言➡直接编译成二进制机器指令;

C#作为高级语言包含了GC垃圾回收, 语法特性, 线程等服务,而 c++不提供语言层面的GC垃圾回收,那又引入了一个新的知识点IL2CPP VM;
IL2CPP VM: IL2CPP的虚拟机,个人理解是一个运行库,提供一些服务:GC垃圾回收等,利用IL2CPP编写一些库,来提供这些服务;

最终可以得出一个公式:
Unity IL2CPP =  IL2CPP编译出来的二进制指令+IL2CPP vm(GC,Thread等)

HybridCLR热更新个人理解

目前市面上主流的热更新方案:

  • Lua: 内置Lua虚拟机 + Lua代码
  • ILRuntime: 内置C#的虚拟机 + 解释执行ILRuntime;

内置虚拟机什么意思? 
自己解释执行的一个运行环境,无法直接继承Monobehaviour需要自定义封一层:跨域访问,接口导出都需要开发者自己来处理,都是一件很繁琐的事情,不符合我们标准的Unity开发(拖,拉,拽,哈哈)

HybridCLR到底做了什么事情

IL2CPP runtime环境(IL2CPP VM)编写了一个解释器,解释执行IL代码指令 + 使用的是AOT的数据内存对象

我们对比下IL2CPP和HybridCLR区别
假设: 我们先定义一个struct GameObject数据内存:

C/C++代码 
AddName(GameObject go){
        string a = "test";
        return a + go.ToString();
}

IL代码
AddName(GameObject go){
        string a = "test";
        return a + go.ToString();
}

HybridCLR代码
Extra_AddName_ILFunc(GameObject  obj){
        加载到IL指令:
        {
                定义一个string类型的变量a;
                返回 a + obj.ToString();;
        }
}

得出:
IL2CPP = 数据内存(GameObject) + 代码逻辑(二进制机器指令);
HybridCLR = 数据内存(GameObject) + 代码逻指(二进制机器指令) + IL代码指令解释执行

这就是在HybridCLR热更的项目中,我们可以随意的继承使用我们GameObject,Monobehaviour;
因为这些数据对象在编译AOT的时候,这些类型全部编译进去了;
解释执行IL的new GameObiect 和 new AOT的GameObiect对象是一摸一样的;

HybridCLR优势有哪些?

  1. 直接使用的时候我们的AOT中的内存对象,内存的占用,跨域都没有什么问题; 这点就解决了其他热更项目所无法解决的内存优化,跨域问题,意味着性能提高了
  2. 不用改变我们标准的Unity开发(拖,拉,拽)习惯
  3. 不需要额外写任何特殊代码、没有代码生成,几乎没有限制。

HybridCLR开发环境搭建

快速上手 | Focus Creative GamesFocus Creative Games是一个业内领先的专注于提供游戏行业深度技术解决方案的创新型公司。https://focus-creative-games.github.io/hybridclr/start_up/开发环境准备:

  • 支持2019.4.x、2020.3.x、2021.3.x系列LTS版本
  • Visual Studio 2019 C++ 的程序集;
  • Android NDK:
  • IL2CPP for windows: 安装好测试平台的IL2CPP
  • 安装git
  • HybridCLR环境准备:

     hybridclr  核心仓库, IL代码指令解释执行
     il2cpp_plus il2cpp补丁仓库,不含核心代码是核心代码运行的必要环境
     hybridclr_trial 仓库为示例仓库

    接下来一步步配置:

  • 打开PackageManager=>选择git url
    https://img-blog.csdnimg.cn/0e432a41e3a84f81b31cf5d7671f6ce3.png
  • 输入: GitHub - focus-creative-games/hybridclr_unity: Unity package for HybridCLR
    https://img-blog.csdnimg.cn/950f6c79cbe44da58fb38adffdcffb5c.png
    安装失败查看是否是缺少git环境
  • 找到Installer并安装
    https://img-blog.csdnimg.cn/b96f4bbf93aa49cbaba058725bc7e413.png
    https://img-blog.csdnimg.cn/756561c0034f4cbe92d36a90952b161d.png
  • 控制台看到安装完成
  • 找到setting下热跟新dlls,里面配置所自己需要的热更新dll



    这里我简单的说下我的思路, 由自己的框架层来调用Assembly Definition文件, 我这边叫做MyGame.Runtime, 顾名思义运行时候的内置程序集不作为热更新的dll, 从这里来调用热更程序集MyGame.Logic。
  • 这里着重理解下补充元数据AOT dlls
    il2cpp在完成IL到c++代码的转换后,丢失了原始IL函数体信息, 导致无法根据泛型基类List<T>的元数据实例化出List<float>的各个成员函数实现,所以需要把MyGame.Logic中用到的值类型泛型数据提前补充到AOT里,MyGame.Logic程序就能正确识别那些值类型泛型数据。
    例如:
            List<T> where T : 值类型, 从属于System.Collections.Generic; 主dll为System.dll, 所以需要把System填写入补充元数据AOT dlls中; 这样在MyGame.Logic中加载补充元数据dll后就可以愉快的使用List<T>。

    注意:这部分补充元数据.dll

    详细内容请查阅以下文章:
    处理AOT泛型 | Focus Creative Games
    AOT泛型问题 | Focus Creative Games
  • 使用下面之前先build一次项目, 再选择all
  • iOS平台的特殊处理
    打ios包前,需要自行手动替换xcode工程中的libil2cpp.a为扩充了HybridCLR代码libil2cpp.a
  • 将 热更新 dll 和 补充元数据AOT dll 纳入项目的热更新AB资源管理系统
    参考 hybridclr_trial 中Assert\Editor\HybridCLR\BuildAssetsCommand.cs
    核心代码:
           public static void CopyAOTAssembliesToStreamingAssets()
            {
                var target = EditorUserBuildSettings.activeBuildTarget;
                string aotAssembliesSrcDir = SettingsUtil.GetAssembliesPostIl2CppStripDir(target);
                string aotAssembliesDstDir = Application.streamingAssetsPath;
    
                foreach (var dll in SettingsUtil.AOTAssemblyNames)
                {
                    string srcDllPath = $"{aotAssembliesSrcDir}/{dll}.dll";
                    if (!File.Exists(srcDllPath))
                    {
                        Debug.LogError($"ab中添加AOT补充元数据dll:{srcDllPath} 时发生错误,文件不存在。裁剪后的AOT dll在BuildPlayer时才能生成,因此需要你先构建一次游戏App后再打包。");
                        continue;
                    }
                    string dllBytesPath = $"{aotAssembliesDstDir}/{dll}.dll.bytes";
                    File.Copy(srcDllPath, dllBytesPath, true);
                    Debug.Log($"[CopyAOTAssembliesToStreamingAssets] copy AOT dll {srcDllPath} -> {dllBytesPath}");
                }
            }
    
            public static void CopyHotUpdateAssembliesToStreamingAssets()
            {
                var target = EditorUserBuildSettings.activeBuildTarget;
    
                string hotfixDllSrcDir = SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target);
                string hotfixAssembliesDstDir = Application.streamingAssetsPath;
    #if NEW_HYBRIDCLR_API
                foreach (var dll in SettingsUtil.HotUpdateAssemblyFilesExcludePreserved)
    #else
                foreach (var dll in SettingsUtil.HotUpdateAssemblyFiles)
    #endif
                {
                    string dllPath = $"{hotfixDllSrcDir}/{dll}";
                    string dllBytesPath = $"{hotfixAssembliesDstDir}/{dll}.bytes";
                    File.Copy(dllPath, dllBytesPath, true);
                    Debug.Log($"[CopyHotUpdateAssembliesToStreamingAssets] copy hotfix dll {dllPath} -> {dllBytesPath}");
                }
            }
  • 如何加载dll参考:Assets\Main\LoadDll.cs
    加载热更新dll的核心语句:
    System.Reflection.Assembly.Load(BetterStreamingAssets.ReadAllBytes("Assembly-CSharp.dll.bytes"));
  • 加载补充元数据dll
    加载补充元数据dll的核心语句:
    /// <summary>
    /// 为aot assembly加载原始metadata, 这个代码放aot或者热更新都行。
    /// 一旦加载后,如果AOT泛型函数对应native实现不存在,则自动替换为解释模式执行
    /// </summary>
    private static void LoadMetadataForAOTAssemblies()
    {
        List<string> aotMetaAssemblyFiles = new List<string>()
        {
            "mscorlib.dll",
            "System.dll",
            "System.Core.dll",
        };
        /// 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。
        /// 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误
        /// 
        HomologousImageMode mode = HomologousImageMode.SuperSet;
        foreach (var aotDllName in aotMetaAssemblyFiles)
        {
            byte[] dllBytes = BetterStreamingAssets.ReadAllBytes(aotDllName + ".bytes");
            // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
            LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes, mode);
            Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}");
        }
    }
    
  • 创建热更主入口脚本
    using UnityGameFramework.Runtime;
    
    namespace MyGame.Logic
    {
        public class HotfixEntry
        {
            // 不可调用,供给HybridclrComponent使用
            public static void Start()
            {
                Log.Info("<color=lime>热更新层启动.</color>");
                GameEntry.Hybridclr.UpdateCallback = Update;
                GameEntry.Hybridclr.ShutdownCallback = Shutdown;
            }
    
            // 不可调用,供给HybridclrComponent使用
            private static void Update(float elapseSeconds, float realElapseSeconds)
            {
                Log.Info("<color=lime>HotfixEntry.Update.</color>");
            }
    
            // 不可调用,供给HybridclrComponent使用
            private static void Shutdown()
            {
                Log.Info("<color=lime>HotfixEntry.Shutdown.</color>");
            }
        }
    
    }
  • 再MgGame.Runtime中写入一个启动脚本叫HybridclrCpmponent,用于启用MgGame.Logic中的主入口
/// <summary>
/// 加载热更新DLL
/// </summary>
public async void LoadHotfixDLL(GameFrameworkAction complate = null)
{
    SuccessComplate = complate;

#if !UNITY_EDITOR
    for (int i = 0; i < AssemblyUtility.HotfixAssembliesNameReadOnly.Length; i++)
    {
        Log.Warning($"加载:{AssemblyUtility.HotfixAssembliesNameReadOnly[i]}");
        TextAsset dllAsset = await GameEntry.Resource.AwaitLoadAsset<TextAsset>(AssetUtility.GetHotfixDLLAsset(AssemblyUtility.HotfixAssembliesNameReadOnly[i]));
        byte[] dllbytes = dllAsset.bytes;
        Assembly.Load(dllbytes);
    }
#endif

    StartCoroutine(HotfixStart());
}

/// <summary>
/// 开始执行热更新层代码
/// </summary>
private IEnumerator HotfixStart()
{
    yield return new WaitForEndOfFrame();
    Type logic = Utility.Assembly.GetType(AssemblyUtility.HotfixAssembliesEntryFullNameReadOnly);
    if (logic == null)
    {
        Log.Error($"-----加载失败, 未找到{AssemblyUtility.HotfixAssembliesEntryFullNameReadOnly}类-----");
        yield break;
    }

    MethodInfo entry = logic.GetMethod(AssemblyUtility.HotfixAssembliesEntryMainFuncNameReadOnly, BindingFlags.Public | BindingFlags.Static);

    yield return new WaitForEndOfFrame();
    Log.Info("Hotfix主入口加载完毕, 等待进入游戏!");
    SuccessComplate?.Invoke();
    entry.Invoke(null, null);
}

  • 最后, 遇到问题记得查看FAQ, 上例子

FAQ:
常见错误 | Focus Creative GamesFocus Creative Games是一个业内领先的专注于提供游戏行业深度技术解决方案的创新型公司。https://focus-creative-games.github.io/hybridclr/common_errors/#currently-selected-scripting-backend-il2cpp-is-not-installed性能报告

https://focus-creative-games.github.io/hybridclr/benchmark/https://focus-creative-games.github.io/hybridclr/benchmark/


 有兴趣的小伙伴可以关注一波

 o(* ̄▽ ̄*)ブ

猜你喜欢

转载自blog.csdn.net/flj135792468/article/details/129551785