Unity热更新那些事

前言

  • 本文想要给大家分享的是Unity热更那些事儿,会带大家了解
    • 在打包时为什么选择使用Mono作为脚本引擎的后台?
    • JIT与Mono有什么关系?
    • IOS热更新的问题
    • Lua如何进行热更新?
    • ILRunTime热更新介绍

议题

Unity热更新那些事_第1张图片

  • 理想当中的热更新流程
  • 现实中的热更新的流程
  • Unity程序编译和打包方式
  • Unity程序的执行方式
  • IOS平台的App为什么不能热更新
  • 解决方案(能支持所有平台热更新的通用解决方案)

什么是游戏热更新

Unity热更新那些事_第2张图片

  • 游戏热更新的更新流程是什么样的?
    • 1,制作游戏更新内容,可能是新的资料包、新玩法、新道具、数值调整
    • 2,更新内容完成以后,开发者会把它们打包成Unity里的AB包并上传到资源更新服务器上,上传更新内容的同时还要上传更新目录索引,更新目录索引中储存的是文件、模型、贴图资源的索引和资源内容
    • 3,这时玩家打开游戏就会检查更新,并下载更新资源
  • 从软件商店下载游戏App并安装到手机上,启动手机上的游戏时会连接游戏的服务器,检查服务器上面有没有更新文件列表,如果有,就会把更新内容下载下来,没有就不更新,这是游戏玩家的热更新流程
  • 游戏开发者的热更流程是
    • 更新是不是直接把旧的内容覆盖掉或者把新增的内容加上去后游戏就能使用更新以后的内容?
    • 没有那么简单,特别是游戏代码热更新,并不是把最新内容下载完以后就能直接执行到更新以后的内容,中间还有一些过程和步骤,这也是本文要着重讲的内容,也就是上图中的第六步:如何在资源下载完以后执行热更新

游戏热更新的种类

Unity热更新那些事_第3张图片

  • 游戏的热更新分为资源热更新和代码热更新
    • 资源热更新可以参考我们的《Unity小白的游戏梦》,也可以咨询我们的爱丽丝老师
  • 代码热更新
    • 程序员写的程序代码也是一种资源,叫做脚本资源,脚本在Unity中打包以后是以动态链接库文件的形式存在于磁盘上面的,所以代码也是一种资源,可以和其他资源一样按照统一步骤进行热更新
  • 如何更新Unity脚本代码?
    • 要进行代码热更新其实无非就是用新的Unity开发中的代码所形成的同名文件去覆盖旧的同名文件,这是热更新理想中的情况,现实当中并没有这么容易

如何打包?

Unity热更新那些事_第4张图片

  • 很简单,点击Unity文件菜单底下的Build选项就会弹出如上图左边一样的对话框,这个对话框是打包设置的对话框,点击下面的unity引擎设定(PlayerSeting)按钮,就会打开上图右边的属性面板
  • 在属性面板里打包时首先要决定了一个非常重要的选项,就是脚本引擎后台,也就是上图右边的ScriptingBackend,这个选项是一个下拉框,其中有两个选项
    • 1,Mono方式打包
    • 2,IL2CPP方式打包
  • 选择Mono方式打包出来的程序只能支持32位的程序
    • 现在的电脑上预装的Windows系统或者是Mac苹果电脑,包括手机上的操作系统一般都会装64位的系统,那么32位跟64位有什么区别呢?
      • 32位系统所能支持的内存范围比较小,只能支持四个GB的内存,所以大部分人会选择64位系统,它能支持的内存范围比较大
    • 如果使用Mono方式打包,那么就只能打一个32位的系统包,这也就意味着你的程序虽然跑在64位的系统上,但它只能作为一个兼容的32位程序来运行,最多只能支持使用四个G的内存
    • 对于一个大型游戏而言,只使用4个G的内存是完全不够用的,而且内存的空闲空间越大,程序跑起来肯定就更欢乐,所以要注意:使用Mono方式打包的程序不支持64位系统
  • 如果把脚本后台切换为IL2CPP方式就能够支持64位的系统平台
  • 两种打包方式的区别
    Unity热更新那些事_第5张图片
  • Mono打包方式
    • 用Mono方式来进行打包的程序会出现一堆动态链接库,程序员写的程序控制逻辑就在上图左边被红框框住的Assembly-CSharp.dll动态链接库里面
    • 这个动态链接库里包括了所有的功能代码在里面,当要执行程序时,就必须在程序启动之前把它加载到Mono虚拟机里面
      • Mono本身是一个虚拟机,因为C#本身是运行在DotNet平台上面的,而DotNet平台本身就是一个基于虚拟机的的平台,Mono虚拟机是对DotNet虚拟机的跨平台移植
  • IL2CPP打包方式
    • 使用IL2CPP方式打出来的包是没有动态连接库的,它将Mono虚拟机和Assembly-CSharp.dll动态链接库整合在了一起,放在libil2cpp.so文件里(如上图右边最下方的图片)

Mono和IL2CPP的原理详解

Mono方式脚本编译流程

Unity热更新那些事_第6张图片

  • Unity的项目当中可以写很多的C#脚本,C#脚本在打包时会被Mono平台中的C#源码编译器翻译成一种汇编语言
  • 在游戏运行时,这些汇编语言会跟游戏项目中其他的第三方的DLL一起放入Mono虚拟机,由Mono虚拟机来解析并执行这些中间汇编指令,这就是Mono方式编译流程

Unity热更新那些事_第7张图片

  • C#程序经过编译器的被翻译成的中间汇编语言在微软的技术术语里叫做CIL,就是通用中间汇编语言
  • 为什么叫通用中间汇编呢?
    • 因为不管是用C#还是其他语言写的脚本,它们经过翻译以后都是翻译成同一种汇编指令
    • 这也是DotNet框架非常强大的一点,不管是用什么语言写的程序,最终在虚拟机里面执行时它执行的都是相同的一套指令、并且它这套指令是跟操作系统无关的
  • 也就是说它的机器指令跟具体平台是无关的,它是怎么做到的呢?
    • 这种汇编指令相当于指定了一个规范,这个规范的执行是通过CIR中间语言编译器执行的,它会把与具体平台无关的指令翻译成能够在具体平台上执行的跟平台相关的指令
    • 所以不管使用什么样的语言写DoNetT程序它都可以被成功编译,因为它编译以后形成的是中间汇编语言,中间汇编可以运行在各种各样的平台上

Unity热更新那些事_第8张图片

  • 当你打开Unity项目时,VS里会形成四个子项目,这四个子项目分别是
    • Assembly-CSharp(由程序员写的程序逻辑)
    • Assembly-CSharp-Editor(由程序员写的一些编辑器扩展)
    • Assembly-CSharp-Editor-firstpass(针对编辑器扩展的一些插件)
    • Assembly-CSharp-firstpass(第三方的脚本插件)
  • 当Unity自动编译或者程序员手动编译Unity脚本时,它就会形成如上图中间所示的跟项目名称相对应的动态链接库,这些动态链接库里存放了程序源代码对应的机器指令
  • 这些机器指令长就是右边的第三幅图的样子,它是微软汇编语言的指令集所形成的代码

Unity热更新那些事_第9张图片

  • 总结
    • CLI相当于中间汇编语言、CIL相当于微软的虚拟机、CLR是微软平台相关的运行时库,各个平台的功能都包装在这个库里,它们构成了微软的虚拟机
    • 这个虚拟机有两个功能
      • 1,前端把C#代码编译成汇编语言
      • 2,后端在运行时把这些中间汇编语言翻译成具体平台的原生机器指令
    • CLR
      • CLR负责了平台相关的功能,这些功能包括了进程和线程的管理、内存分配、垃圾收集、文件管理等
    • Mono虚拟机
      • 微软传统的虚拟机叫做DotNet虚拟机,DotNet平台本身是不支持跨平台,但经过Mono的移植,使它能够运行在很多平台上,包括常见的安卓、苹果、BSD、Linux、Windows等等
      • 所以Unity能支持跨平台,其实并不是Unity自身的能力,而是利用了开源项目的能力

Unity热更新那些事_第10张图片

  • Mono虚拟机运行汇编语言有三种方式
    • JIT模式
      • 在这种模式下,虚拟机会加载动态链接库文件里的汇编指令,然后进行逐条翻译,翻译成针对某一个特定的手机平台机器指令后交给CPU执行
    • AOT方式
      • AOT方式是在程序编译成中间汇编以后进一步把程序直接编译成针对特定平台的原生机器码,然后运行时交给CPU执行
      • 这种模式下的程序都是提前编译好的,它的缺点就是编译时间长,优点是运行速度快,因为它把所有的程序全部提前编译好了
      • 这种方式还有一个问题:如果采用AOT方式,它有一部分代码还是会在运行时动态编译,这就引出了第三种模式
    • FullAOT模式
      • FullAOT模式也可以叫做完全提前编译模式,它会在程序形成中间汇编以后,把这些中间汇编全部翻译成一些原生码,然后在运行时执行
      • 这样就会形成一个特征,就是使用完全AOT模式编译的代码不会在运行时动态生成任何代码,这件事情究竟是好是坏呢?这要从两方面来看
        • FullAOT模式的优点是:安全性比较好,因为它在运行时不允许动态执行程序代码
        • 缺点:如果想要热更新一些新的程序代码,那么用FullAOT模式就很不方便,因为它不允许动态更新程序代码,而热更新是要求在程序启动时动态加载程序代码的
        • 这也是热更新的困境所在,IOS平台并不支持即时编译,安卓平台则能够支持即时编译,也就是说哪怕你在程序里通过热更藏了一个病毒或者木马,那么安卓手机照样可以执行,这也是它的安全问题所在
        • 而FullAOT模式是完全禁止动态运行的,所以你没有机会把一些从网上下载的病毒木马放到内存里面执行,从而保证了程序安全
        • 安卓系统可以支持上面第三种模式,而IOS由于安全性考虑只支持第三种,所以IOS想要进行热更就不太方便

IL2CPP方式脚本编译流程

Unity热更新那些事_第11张图片

  • IL2CPP方式热更流程的前面几个步骤跟Mono方式是一样的,只是IL2CPP方式不是在代码运行时放到虚拟机去执行,而是进一步再编译,用IL2CPP工具把中间汇编语言转换成C++代码,然后经过C++的本机编译器来进行编译
  • 编译完了以后会形成一些本机可执行的汇编语言代码机器指令,然后会把它交到IL2CPP的虚拟机来执行
  • 所以IL2CPP其实和Mono一样,也是由两部分构成,只不过IL2CPP的编译是翻译成中间汇编后,直接进一步的翻译成C++代码,然后再把C++代码翻译成汇编机器指令,这样IL2CPP在运行时就没有动态编译过程了

Unity对于不同系统平台的脚本后台支持

Unity热更新那些事_第12张图片

  • Unity对于不同的系统平台的脚本后台支持也是不一样的,安卓能同时支持Mono,及时JIT和IL2CPP,而IOS平台就只能使用IL2CPP方式,大部分主机平台也是一样只能采用IL2CPP方式

IOS平台禁止JIT编译

Unity热更新那些事_第13张图片

  • 使用Mono脚本后台编译能够支持AOT、FullAOT和JIT,而使用IL2CPP就只能支持提前编译方式
  • IOS平台的编译选项只能支持FullAOT和IL2CPP方式,但因为FullAOT方式只能支持32位系统,而苹果要求从2016年以后都必须支持64位架构,所以只能采用IL2CPP方式

如何热更?

理想的热更流程

Unity热更新那些事_第14张图片

  • 最为理想的热更流程就是把热更功能写在动态链接库里,然后在程序启动时用新的同名动态链接库覆盖旧的,并通过Assembly.Load动态加载这个动态连接库,最后通过反射来获取到热更DLL里的类型,并创建这个类的实例
  • 这是最理想的热更流程,而且因为安卓平台支持即时编译方式,所以这样的热更流程方式在安卓平台使用没有任何问题,这也是为什么安卓平台编译简单,安全性相对差一点的原因,但这样的流程放在IOS平台上面实施就会失败

IOS禁止为动态分配内存赋予执行权限

#include
#include 
#include 
#include 
#include 

// 分配内存
void* create_space(size_t size){
    void* ptr = mmap(0, size, 
                    PROT_READ | PROT_WRITE | PROT_EXEC,
                    MAP_PRIVATE | MAP_ANON,
                    -1, 0);
    return ptr;
} 

// 在内存中创建函数
void copy_code_2_space(unsigned char* m){
    unsigned char macCode[] = {
        0x48, 0x83, 0xc0, 0x01,
        c3
    };
    memcpy(m, macCode, sizeof(macCode));
}

// main 声明一个函数指针TestFun用来指向我们的求和函数在内存中的地址
int main(int argc, char**, argv){
    const size_t SIZE = 1024;
    typedef long (*TestFun)(long);
    void* addr = create_space(SIZE); // 通过create_space创建一块内存大小为1024的内存
    copy_code_2_space(addr); // 向addr中写入函数
    TestFun test = addr; // 
    int result = test(1);
    print("result = %d\n", result);
    return 0;
}
  • 上面是我准备的一段实验代码,它是在苹果电脑上执行的,这段代码做了什么事情呢?
    • 这段代码中create_space函数是一个内存分配函数,它会按照指定的字节数来创建一个内存映射文件,创建内存映射文件可以理解为创建一块内存,这块内存具有执行权限,创建好这块内存后会返回这块内存区域
    • 内存分配函数下面的copy_code_2_space函数的功能是指定一个地址,然后往地址里写一段程序代码,这个代码很简单,可以把它理解成一个两个数相加的代码,或者是简单的赋值,函数中的memcpy就是把这段程序代码的机器指令拷贝到m所代表的内存里
    • 最下方main函数指定一块内存,大小是1024,然后通过create_space按照这个内存大小来创建一块内存,创建了这块内存地址以后就往这块内存地址里写入了一个函数
  • 在Mac电脑中执行这段代码时会得到运行报错,为什么会报错?
    • 因为代码中的分配内存函数为这块内存赋予了执行权限,而IOS平台是禁止为动态分配的内存块赋予执行权限的,这就是为什么IOS平台无法通过动态加载程序代码进行热更新的原因

Unity热更新那些事_第15张图片

  • 所以IOS只能采用静态编译方式,静态编译方式有两种方案:Full-AOT和IL2CPP,但即使采用了这两种方式的任意一种,也不能完全避免一些由于不合理使用代码所带来的问题
  • 下面是一段IOS平台上的程序

Unity热更新那些事_第16张图片

  • 在这段程序中我创建一个管理器接口和一个接收者接口,管理器接口可以发送消息,接收者接口可以响应消息
  • 通过管理器接口继承实现管理器类,在这个管理器类里实现SendMessage方法,当管理器类实现SendMessage方法时,它会对IReceiver类型的target对象调用OnMessage方法,传入的参数是泛型类型的value,这里会出现一些问题
    • 调用SendMessage方法时消息会执行到OnMessage方法,问题在于OnMessage的参数是泛型类型
    • 由于采用了Full-AOT和IL2CPP来进行编译,而泛型代码由于在运行之前无法提前得知泛型的实际数据类型,所以当Mono虚拟机以Full-AOT的方式执行编译代码,或者是IL2CPP虚拟机执行这样代码时就会直接跳过OnMessage的执行
    • 因为Full-AOT方式包括和ILL2CPP方式是静态编译的,所以不能执行这些在程序运行当中动态指定类型的代码
  • 所以实际上ILL2CPP根本就没有把泛型类型对应的实际类型代码编译到最终程序当中,当你执行OnMessage时就会看到下面的报错,意思是你尝试在AOT编译的程序中执行动态类型的代码

在这里插入图片描述
Unity热更新那些事_第17张图片

  • 要解决这个问题可以强制AOT编译系统在程序运行之前提前生成针对某种类型的代码,比如你发送消息时要发送的类型是AnyEnum类型的消息,那么就可以提前写一段OnMessage(AnyEnum.Zero)
  • 这样就会强制IL2CPP编译这段代码,从而生成针对AnyEnum类型的程序代码,但是这样做也就丧失了泛型的灵活性

Unity热更新那些事_第18张图片

  • 脚本限制其实还是很多的,大家如果想要具体的了解IOS是如何禁止动态内存执行的,可以参考一下我们的《Unity小白的游戏梦》课程,如果想要了解如何通过反射像病毒一样动态生成代码也可以参考一下《Unity小白的游戏梦》课程

热更新的难点和解决方案

解决方案

Unity热更新那些事_第19张图片

  • 既然IOS平台有这么多的限制,那么应该怎么去应对IOS的平台限制呢?
    • 有一个简单粗暴的方法,就是不为IOS平台准备热更新功能,只为安卓平台准备热更新功能,但这种方案可行吗?
    • IOS的用户按照传统认为都是一些高价值用户,而且就算不考虑高价值,IOS的用户至少也是占到手机市场三分之一的,任何一个开发市商都不会放弃这部分用户
  • 所以不针对IOS做热更是不行的,那么应该怎样针对IOS做热更呢?
    • 我们的解决方案是嵌入一种脚本语言
    • 嵌入的脚本语言有两种
      • 1,Lua,这是比较传统的热更方案,很多PC端的游戏都是使用Lua方式进行热更的,Lua热更方案有两种,一种是ToLua,一种是XLua
      • 2,C#热更方案,C#热更方案是比较有前景的一种方式,因为ILRunTime热更已经被加入到Unity官方的PackageManager里面了
  • 为什么脚本语言可以热更?
    • 脚本语言的工作原理就是每一次启动游戏时都要在服务器上检测一下有没有程序脚本更新,有就从服务器上把脚本下载到本地客户端,下载完以后客户端启动时就会把脚本加载到内存里执行
    • 但刚才也说了IOS平台禁止动态分配一块内存执行其中的代码,为什么两种方案的代码能加载到内存里面并执行呢?限于篇幅问题,我会在我们的《Unity小白的游戏梦》课程里花上一个专题,专门给大家介绍这块内容

小结

Unity热更新那些事_第20张图片

  • 1,Unity脚本后台有哪几种?
  • 2,每种脚本后天分别支持哪几种编译方式?
  • 3,安卓/苹果分别支持哪几种编译方式?
  • 4,C#脚本对反射的使用有限制,那么什么样的反射方法可以使用呢?
  • 5,Lua/ILRunTime热更方案都会把脚本加载到内存并执行,但是为什么这两种方式就能正常执行动态加载的脚本呢?

写在最后

  • 本文为“优梦创客”原创文章,您可以自由转载,但必须加入完整的版权声明
  • 点赞、关注、分享可免费获得配套学习资源
  • 点击观看完整视频

你可能感兴趣的:(热更新,Unity教程,unity,ILRunTime,Lua,Mono,IL2CPP)