Unity 面试篇|(五)热更新与Lua语言篇 【全面总结 | 持续更新】

发布时间:2024年01月14日

1.什么是热更新?

  • 热更新 是一种App软件开发者常用的更新方式。简单来说,就是在用户通过下载安装APP之后,打开App时遇到的即时更新。
  • 在安卓、iOS平台,热更新表示在更新游戏资源或逻辑的时候不需要开发者将游戏再打包、上传、审核、发布、玩家重新下载安装包更新游戏,仅需要开发者打出新的ab(AssetBundle)资源文件放到网上,然后游戏程序下载新的ab资源文件替换本地的资源文件来实现游戏更新的流程。
  • 热更代码可以理解成是特殊的资源。

2.主流的代码热更方案有哪些?

  • LUA热更(xLua/toLua等)(LUA与C#绑定,方案成熟)
  • ILRuntime热更
  • puerts
  • HyBridCLR(原huatuo)
  • iOS:IL2CPP,AOT(Ahead of Time,运行前编译)
  • 安卓:Mono,JIT(Just in Time,动态(即时)编译)
  • DLL基于动态即时编译,只能在JIT模式下使用,即无法在iOS平台使用
    lua有自己的虚拟机和运行时解释器,不受限于编译方式(IL2CPP、Mono)
    ILRuntime和LSharp也有自己的虚拟机和运行时解译引擎,也不受限于编译方式

3.AssetBundle介绍

  • AssetBundle是将资源使用Unity提供的一种用于存储资源的压缩格式打包后的集合,它可以存储任何一种Unity可以识别的资源,如模型,纹理图,音频,场景等资源。也可以加载开发者自定义的二进制文件。
  • 用途:
    • 制作DLC (动态的可下载内容)
    • 减少初始包大小
    • 加载为用户平台优化的资源
    • 减少运行时的内存压力

4.AssetBundle的具体开发流程

  • 创建Asset bundle,开发者在unity编辑器中通过脚本将所需要的资源打包成AssetBundle文件。
    上传服务器。开发者将打包好的AssetBundle文件上传至服务器中。使得游戏客户端能够获取当前的资源,进行游戏的更新。
  • 下载AssetBundle,首先将其下载到本地设备中,然后再通过AsstBundle的加载模块将资源加到游戏之中。
  • 加载,通过Unity提供的API可以加载资源里面包含的模型、纹理图、音频、动画、场景等来更新游戏客户端。
  • 卸载AssetBundle,卸载之后可以节省内存资源,并且要保证资源的正常更新。

5.AssetBundle的压缩格式

  • LZMA格式: 使用LZMA格式压缩的AssetBundle的包体积最小(高压缩比),但是相应的会增加解压缩时的时间。
  • LZ4格式:压缩后的AssetBundle包体的体积较大(该算法基于chunk)。但是使用LZ4格式的好处在于解压缩的时间相对要短。
  • 不压缩:没有经过压缩的包体积最大,但是访问速度最快。

6.AssetBundle对象的加载方式

  • Unity提供了三个不同的API从AssetBundles加载UnityEngine.Objects,这些API都绑定到AssetBundle对象上,并且这些API具有同步(和异步变体):
    • LoadAsset(LoadAssetAsync):从资源包中加载指定的资源
    • LoadAllAssets (LoadAllAssetsAsync):加载当前资源包中所有的资源
    • LoadAssetWithSubAssets (LoadAssetWithSubAssetsAsync)
  • 并且这些API的同步版本总是比异步版本快至少一个帧(其实是因为异步版本为了确保异步,都至少延迟了1帧),异步加载每帧会加载多个对象,直到它们的时间切片切出。

7.ssetBundle资源卸载

  • AssetBundle.Unload(false):内存中的AssetBundle对象包含的资源会被销毁。
  • AssetBundle.Unload(true):不仅仅内存中的AssetBundle对象包含的资源会被销毁。根据这些资源实例化而来的游戏内的对象也会销毁。
  • Reources.UnloadAsset(Object):显式的卸载已加载的Asset对象,只能卸载磁盘文件加载的Asset对象Resources。
  • UnloadUnusedAssets:用于释放所有没有引用的Asset对象
  • Destroy:主要用于销毁克隆对象,也可以用于场景内的静态物体,不会自动释放该对象的所有引用。虽然也可以用于Asset,但是概念不一样要小心,如果用于销毁从文件加载的Asset对象会销毁相应的资源文件!但是如果销毁的Asset是Copy的或者用脚本动态生成的,只会销毁内存对象。

8.资源如何打包?依赖项列表如何生成?

  • 查找指定文件夹ABResource里的资源文件
    • Directory.GetFile(资源路径)
    • 新建AssetBundleBuild对象
    • 获取资源名称,并赋值对应AB名称
    • 获取各个资源的依赖项:通过UnityEditor.AssetDataBase类获取各个资源的依赖项
  • 使用Unity自带的BuildPipeline进行构建AB包
    • BuildPipeLine.BuildAssetBundles(输出AB包路径)
    • File.WriteAllLines(将依赖项写入文件里)

9.如何解析版本文件?如何加载AB包资源?具体流程是怎么样的?

  • 解析版本文件列表
    • File.ReadAllLines(读取文件列表资源路径URL)
    • 获取资源名称,获取AB包名称,获取依赖项,字典容器存储
    • 获取Lua文件
  • 加载资源
    • 异步加载资源AB包,AssetBundleRequest请求,AssetBundle.LoadFromFileAsync
      先检查依赖项,再异步加载AB包依赖项
    • 加载成功后都有对应的回调方法,将资源作为参数传入

10.热更新打包方案有哪些?

  • 整包:将完整更新资源放在Application.StreamAssets目录下,首次进入游戏将资源释放到Application.persistentDataPath下。
    • 优点:首次更新少
    • 缺点:安装包下载时间长,首次安装久
  • 分包:少部分资源放在包里,其他资源存放在服务器上,进入游戏后将资源下载到Application.persistentDataPath目录下。
    • 优点:安装包小,安装时间短,下载快
    • 缺点:首次更新下载解压缩包时间旧
  • 适用性
    • 海外游戏大部分是使用分包策略,平台规定
    • 国内游戏大部分是使用整包策略

11.热更新的流程

  • 导出热更流程
    • 打包热更资源的对应的md5信息(涉及到增量打包)
    • 上传热更 ab 到热更服务器
    • 上传版本信息到版本服务器
  • 游戏热更流程
    • 启动游戏。
    • 根据当前版本号,和平台号去版本服务器上检查是否有热更。
    • 从热更服务器上下载 MD5 文件,比对需要热更的具体文件列表。
    • 从热更服务器上下载需要热更的资源,解压到热更资源目录。
    • 游戏运行加载资源,优先到热更目录中加载,再到母包资源目录加载。

11.1简述Lua实现面向对象的原理

  • 表table就是一个对象,对象具有了标识self,状态等相关操作
  • 使用参数self表示方法的该接受者是对象本身,是面向对象的核心点,冒号操作符可以隐藏该self参数
  • 类(Class):每个对象都有一个原型,原型(lua类体系)可以组织多个对象间共享行为
  • setmetatable(A,{__index=B}) 把B设为A的原型
  • 继承(Inheritance):Lua中类也是对象,可以从其他类(对象)中获取方法和没有的字段
  • 继承特性:可以重新定义(修改实现)在基类继承的任意方法
  • 多重继承:一个函数function用作__Index元方法,实现多重继承,还需要对父类列表进行查找方法,但多继承复杂性,性能不如单继承,优化,将继承的方法赋值到子类当中
  • 私有性(很少用)基本思想:两个表表示一个对象,第一个表保存对象的状态在方法的闭包中,第二个表用来保存对象的操作(或接口),用来访问对象本身。使第一个表完成内容私有性。

12.简述Lua有哪8个类型?简述用途

  • nil 空——可以表示无效值,全局变量(默认赋值为nil),赋值nil ,使其被删除。
  • number 整数
  • table 表
  • string 字符
  • userdata 自定义
  • function 函数
  • bool 布尔
  • thread线程

13.C#与Lua的交互原理简述

  • 想要理解Lua语言与其它语言交互的实质,我们首先就要理解Lua堆栈。
    简单来说,Lua语言之所以能和C/C++进行交互,主要是因为存在这样一个无处不在的虚拟栈。栈的特点是先进后出,在Lua语言中,Lua堆栈是一种索引可以是正数或者负数的结构,并规定正数1永远表示栈底,负数-1永远表示栈顶。
    换句话说,在不知道栈大小的情况下,我们可以通过索引-1取得栈底元素、通过索引1取得栈顶元素。
  • Lua是一种嵌入式脚本语言,可以方便的与c/c++进行相互调用。但是Unity中主要是用c#进行开发的,因此在Unity中使用Lua通常有以下两种方案:
    • 使用c#实现一个lua虚拟机
    • 基于原生的c lua api做一个封装,让c#调用
  • 从性能上考虑,当前主流方案都是第二种。
    基于第二种方案实现的框架目前主要有xLua,sLua,uLua,NLua(+KeraLua)。在这些方案中,都能找到一个相关的类,封装了c#对lua c api的调用。例如在xlua中是XLua.LuaDLL.Lua这个类,在slua中是SLua.LuaDll这个类。
    所以在Unity里执行Lua是以c作为中间媒介的:
C# <=> C <=> Lua
  • Lua与宿主语言(这里以c#为例)最基础的两种交互模式即:
    • c#执行lua代码
    • lua执行c#静态/成员函数
  • 这种交互是通过一个栈结构进行的。

14.Lua中 pairs与ipairs区别

  • pairs会遍历所有key,对于key的类型没有要求,遇到nil时可以跳过,不会影响后面的遍历,既可以遍历数组部分,又能遍历哈希部分。
  • ipairs只会从1开始,步进1,只能遍历数组部分, 中间不是数字的key忽略, 到第一个不连续的数字为止(不含),遍历时只能取key为整数值,遇到nil时终止遍历。

15.Lua中 点和冒号区别

  • 点 :无法传递自身,需要显示传递
  • 冒号 :隐式传递自身

16. Lua深拷贝和浅拷贝

  • 如何实现浅拷贝
    使用 = 运算符进行浅拷贝
    • 拷贝对象是string、number、bool基本类型。拷贝的过程就是复制黏贴!修改新拷贝出来的对象,不会影响原先对象的值,两者互不干涉。
    • 拷贝对象是table表,拷贝出来的对象和原先对象时同一个对象,占用同一个对象,只是一个人两个名字,类似C#引用地址,指向同一个堆里的数据~,两者任意改变都会影响对方。
  • 如何实现深拷贝
    复制对象的基本类型,也复制源对象中的对象
    常常需用对Table表进行深拷贝,赋值一个全新的一模一样的对象,但不是同一个表。
    Lua没有实现,封装一个函数,递归拷贝table中所有元素,以及设置metetable元表。
    如果key和value都不包含table属性,那么每次在泛型for内调用的Func就直接由if判断返回具体的key和value。
    如果有包含多重table属性,那么这段if判断就是用来解开下一层table的,最后层层递归返回。

17.Lua中的闭包简述

  • 闭包=函数+引用环境
    子函数可以使用父函数中的局部变量,这种行为可以理解为闭包!
    • 闭包的数据隔离
      不同实例上的两个不同闭包,闭包中的upvalue变量各自独立,从而实现数据隔离
    • 闭包的数据共享
      两个闭包共享一份变量upvalue,引用的是更外部函数的局部变量(即Upvlaue),变量是同一个,引用也指向同一个地方,从而实现对共享数据进行访问和修改。
    • 利用闭包实现简单的迭代器
      迭代器只是一个生成器,他自己本身不带循环。我们还需要在循环里面去调用它才行。
      • while…do循环,每次调用迭代器都会产生一个新的闭包,闭包内部包括了upvalue(t,i,n),闭包根据上一次的记录,返回下一个元素,实现迭代
      • for…in循环,只会产生一个闭包函数,后面每一次迭代都是使用该闭包函数。内部保存迭代函数、状态常量、控制变量。

18. __index和__newindex元方法的区别

  • __newindex用于表的更新,__index用于表的查询。
    • 如果访问不存在的数据,由__index提供最终结果
    • 如果对不存在的数据赋值,由__newindex对数据进行赋值
  • __index元方法可以是一个函数,Lua语言就会以【表】和【不存在键】为参数调用该函数
  • __index元方法也可以是一个表,Lua语言就访问这个元表
    对表中不存在的值进行赋值的时候,解释器会查找__newindex
  • __newindex元方法如果是一个表,Lua语言就对这个元表的字段进行赋值

19.table的一些知识点

  • table 是 Lua 的一种数据结构,用于帮助我们创建不同的数据类型,如:数组、字典等;
  • table 是一个关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil,所有索引值都需要用 “[“和”]” 括起来;如果是字符串,还可以去掉引号和中括号; 即如果没有[]括起,则认为是字符串索引,Lua table 是不固定大小的,你可以根据自己需要进行扩容;
  • table 的默认初始索引一般以 1 开始,如果不写索引,则索引就会被认为是数字,并按顺序自动从1往后编;
  • table 的变量只是一个地址引用,对 table 的操作不会产生数据影响;
  • table 不会固定长度大小,有新数据插入时长度会自动增长;
  • table 里保存数据可以是任何类型,包括function和table;
  • table所有元素之间,总是用逗号 “,” 隔开;

20.Lua是如何实现热更新的

  • Lua的模块加载机制,热更的核心就是替换Package.loaded表中的模块。
  • 导出函数require(mode_name)
  • 查询全局缓存表package.loaded
  • 通过package.searchers查找加载器
    • package.loaded
      存储已经被加载的模块:当require一个mode_name模块得到的结果不为假时,require返回这个存储的值。require从package.loader中获得的值仅仅是对那张表(模块)的引用,改变这个值并不会改变require使用的表(模块)。
    • package.preload
      保存一些特殊模块的加载器:这里面的值仅仅是对那张表(模块)的引用,改变这个值并不会改变require使用的表(模块)。
    • package.searchers
      require查找加载器的表:这个表内的每一项都是一个查找器函数。当加载一个模块时,require按次序调用这些查找器,传入modname作为唯一参数。此方法会返回一个函数(模块的加载器)和一个传给这个加载器的参数。或返回一个描述为什么没有找到这个模块的字符串或者nil。

21.Lua的sort排序用的是什么排序方法

快速排序

public void QuickSort(int[] arr,int low,int high){
    if (low>high)
        return;
    int pivot = partition(arr,0,arr.Length()-1);
    QuickSort(arr,low,pivot-1);
    QuickSort(arr,pivot+1,high);
}
public int partition(int[] arr,int low,int high){
    int pivot = arr[low];
    while(low<high){
        while( low < high && arr[high] >= pivot) high--;
        arr[low] = arr[high];
        while(low > high && arr[low] <= pivot) low++;
        arr[high] = arr[low];
    }
    arr[low] = pivot;
    return low;
}

22. Lua和C#的交互原理

  • C# -->Lua:由C#先将数据放入栈中,由lua去栈中获取数据,然后返回结果数据到栈顶,再由栈顶返回至C#。
  • Lua -->Wrap -->C#:先生成C#源文件所对应的Wrap文件或者编写C#源文件所对应的C模块,然后将源文件内容通过Wrap文件或者C模块注册到Lua虚拟机中,然后由Lua去调用这个模块的函数
文章来源:https://blog.csdn.net/backlighting2015/article/details/135427314
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。