赞
踩
内存管理是程序开发的核心问题,而资源的使用又与内存息息相关,因此本章想要梳理整个开发流程中Unity对于内存与资源的管理方式。
1.1 基础概念
内存是暂时存放CPU中的运算数据,与硬盘等外部存储器交换的数据。在操作系统中,内存分为物理内存与虚拟内存。
CPU读取数据
CPU进行数据处理时,从内存或缓存中取出指令,放入指令寄存器,并对指令译码进行分解,进而对数据进行处理。从内存中读取数据很慢,通常CPU会将之前读取的数据缓存在多级Cache中,提升数据访问效率。因此,CPU会先从Cache中查找数据,若没有找到(Cache Miss),才会访问内存。
物理内存:物理内存可以理解为运行时内存,其对应于实际的存储硬件。
虚拟内存:管理不同进程使用的进程内部内存地址和物理内存地址的映射体系。
页:内存管理、映射中的基本单位。
内存交换(页面交换):当系统内存不足时,操作系统会创建交换文件(Swap file)或者分页文件(Paging file)记录暂时不用的物理内存页面中的数据,并将这部分数据交换到硬盘上,节省更多的物理内存。
1.2 移动设备与PC差异
1.3 Android内存分布
上图可以看出运行时内存包括:方法区、堆栈以及线程内存。
Android中对内存有4种统计方式:VSS(虚拟内存)、RSS(实际物理内存:APP+所有共享内存)、PSS(APP+平均共享库内存)、USS(APP内存)。通常使用PSS来统计。
Android对进程的管理策略称为LMK(Low Memory Killer),kill优先级如下:
1.4 IOS内存分布
IOS内存分布
Clear Memory包括系统框架数据、可执行的二进制数据以及内存映射文件。Clear Memory以外的内存都是Dirty Memory。
当系统存在内存压力时,会先卸载一部分Clear Memory,当再次需要时会对其重新创建。但系统无法对Dirty Memory进行卸载,直到到达App的限制才会被终止回收。内存不足时,IOS会对Dirty Memory做压缩处理。
IOS中Resident Memory指的是App分配的物理内存。当App向系统申请内存时,虚拟内存是直接增长的。但如果申请完的内存并没有向里面写入数据,它并不会产生实际的物理内存分配。
iOS的进程管理策略称为Jetsam,GitHub有人测试了其kill进程的优先级:
Jetsam优先级
从Editor与Runtime比较,在Runtime时,只有Load资源才会影响内存,而Editor模式下,为了方便开发,只要打开Unity资源就会被加载(Unity打开很慢,2019.3的Asset Pipeline 2.0做了优化)。
2.1 Mono与IL2CPP的跨平台
Mono与IL2CPP是Unity的两种跨平台解决方案,目前官方推荐使用IL2CPP。
Mono跨平台流程
Mono的跨平台方案:编译器mcs将代码编译为IL,通过Mono运行时中的编译器将IL编译成对应平台的原生码。
Unity Mono是针对每个特定平台做了处理,因此才能跨平台。若出现新的硬件平台,则需要额外造轮子,其可移植性很差,因此Unity给出了IL2CPP方案。并且IL2CPP相比Mono有一定的性能提升。
IL2CPP方案流程(来自官方文档)
编译工程时,IL2CPP将Unity Scripting API 代码编译为常规 .NET DLL(托管程序集)。将所有托管程序集转换为标准C++代码。使用本机平台编译器编译生成的C++代码和IL2CPP的运行时部分。最后将代码链接到可执行文件或 DLL,具体取决于目标平台。
IL2PP相比于Mono,最大的变化在于由JIT编译变为了AOT编译。由此带来的变化体现在:(1)开发编译时间变长;(2)无法动态生成代码;(3)应用程序体积变小;(4)应用程序启动时间短。
在开发过程中可以选择Mono,提升开发效率,发布应用时使用IL2CPP得到更好的程序性能。
2.2 Unity VM GC
当Unity VM需要分配内存时,会检查当前内存。若内存足够时,可以直接分配内存,但内存不足时会触发GC,再次检测内存。若此时内存仍然不足,VM会向操作系统申请内存。
触发GC时,VM会暂停所有线程,可能引起程序卡顿。Unity采用了BOEHM处理GC,其为标记清除法。在标记阶段通过访问根节点,并遍历到叶子节点,最终将所有存在引用的内存都标记出来,清除未被标记的托管内存。
Unity的VM以Block管理内存,当一个Block连续6次GC没有被访问到(很难触发),这块内存会被返回给系统。
由于此种处理方式没有内存压缩,并且可能出现内存黑名单,因此容易出现内存碎片。在开发过程中,要尽量减少内存的申请,申请的内存尽量保持连续,对于资源加载应该优先加载较大的资源。
2.3 Unity内存分布
Unity内存分布
Unity内存分为Native堆内存、第三方库、虚拟机以及GfxDriver。Native堆内存中存储由Unity底层管理的数据,如音频、视频、Texture、Mesh、GameObject等。第三库常见的如tolua、Wwise。虚拟机通常指C#所产生的内存。GfxDriver是指当前驱动使用的Textures、RenderTargets、Shaders和Mesh数据内存。
Unity 中的资源工作流程(来自官方文档)
Unity中的资源通常是指3D模型、音频文件、图像等用于程序使用的数据(代码数据也是一种资源)。程序开发阶段,将外部资源导入或复制到Asset目录下,Unity会监听到所导入的数据,并生成相应的Unity默认资源。
3.1 创建asset
Unity检测到新资源(文件夹)时会向此资源分配GUID,这是Unity在内部使用的ID,用于索引资源,以便Unity移动或重命名此资源不会破坏引用关系。
Unity对导入的外部文件格式不会进行修改,对内置格式资源(prefab/material/unity等),Unity以YAML格式进行存储。
prefab在Unity中的存储格式
文件前两行是文件格式版本注释,后续以--- !u!classID &fileID对组件数据进行标记。classID是Unity为组件类型分配的id,fileID为prefab中组件实例化的局部id,由此可以进行索引。上图GameObject挂载了3个组件,所以可以看到m_Component通过fileID映射到相应的组件实例。由于EventSystem是自定义组件,由此可以看到m_Script通过guid来引用到相应的代码资源。
由于这种格式化的存储方式,在非Unity Editor情况下处理或者查找带有特定参数的资源,可以通过纯文本替换或查找处理。(需要以下设置)
以文本格式存储序列化资源
3.2 监听资源导入
Unity提供了用于监听资源导入的基类,方便开发者对资源做标准化处理,具体基类如下:
来自Unity用户手册
3.3 资源处理
Unity在导入资源后,会在Library文件夹下生成真正用于Unity处理的资源。例如,Unity 将 .png 图像文件导入为纹理时,在运行时不会使用原始的 .png 格式数据,而是创建新的图形格式,将其存储在项目的Library文件夹中。Unity的Texture类会使用此导入版本,然后Unity将其上传到GPU进行实时显示。
除了Library,Unity会在Asset文件夹相同位置生成资源的.meta文件,用以记录资源设置参数与GUID。
meta文件
在Unity 2020以前的版本可以通过guid,在Library下搜索到Unity生成的资源。在Unity 2020之后的版本,会在Library下生成ArtifactDB和SourceAssetDB文件。
SourceAssetDB包含.meta相关数据(上次修改日期、文件内容哈希、GUID和其他元数据信息),由此判断是否需要重新导入资源。
ArtifactDB包含每个源资源的导入结果的信息。每个Artifact都包含导入依赖项信息、Artifact元数据信息和 Artifact文件列表。
从上述资源加载流程来看,项目中经常遇到资源引用丢失:(1)美术资源与meta没有一起上传,导致prefab无法通过guid找到资源。(2)美术替换资源时,先删除原有资源,再导入创建新资源,导致原有的prefab引用的guid失效。
对于漏传meta的情况,可在提交版本文件时做强制检测,若Asset下新增资源必须要有对应的meta文件。对于美术资源的替换,不能在Unity Editor下直接删除文件,需要通过import替换,或者在文件夹中替换资源再打开Unity。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。