内存基础
CPU读取数据
CPU进行数据处理时,从内存或缓存中取出指令,放入指令寄存器,并对指令译码进行分解,进而对数据进行处理。从内存中读取数据很慢,通常CPU会将之前读取的数据缓存在多级Cache中,提升数据访问效率。因此,CPU会先从Cache中查找数据,若没有找到(Cache Miss),才会访问内存。
移动设备与PC差异
- 没有独立显卡,显存与CPU数据会存储在同一区域。
- CPU的Cache与内存更小。因此移动端会严格限制资源大小。
- 没有内存交换(IO速度慢、可擦写次数少)
- IOS可以进行内存压缩:将不活跃的内存压缩到特定空间。
Unity内存与GC
- Editor:只要打开Unity资源就会被加载
- Runtime:只有Load资源才会影响内存
Unity内存分布
Native堆内存:Unity底层管理的数据,如音频、视频、Texture、Mesh、GameObject等
第三方库:tolua、Wwise等
虚拟机:C#所产生的内存
GfxDriver:当前驱动使用的Texture、Shader、Mesh数据内存等
Mono与IL2CPP的跨平台
Mono与IL2CPP是U3D的两种跨平台解决方案。
Mono
概念:编译器mcs将代码编译为IL,通过Mono运行时中的编译器将IL编译成对应平台的原生码。
缺点:移植性差。
优点:开发编译时间较短,适用于开发过程。
IL2CPP
概念:编译工程时,IL2CPP将代码编译为.NET DLL托管程序集,再将托管程序集转换为标准C++代码,再交给各平台的C++编译器进行编译。
优点:
- JIT编译 -> AOT编译
- 应用程序体积变小
- 应用程序启动时间短
缺点: - 无法动态生成代码
- 开发编译时间变长
Unity VM GC
当Unity VM需要分配内存的时候,会检查当前内存。若内存足够,可以直接分配内存,但内存不足时会触发GC,再次检测内存,若此时内存仍然不足,VM会向操作系统申请内存。
触发GC时,VM会暂停所有线程,可能引起程序卡顿。
Unity采用标记清除算法开处理GC,管理内存。
Unity的VM以Block管理内存,当一个Block连续6次GC没有被访问到(很难触发),这块内存会被返回给系统。
由于此种处理方式没有内存压缩,并且可能出现内存黑名单,因此容易出现内存碎片。在开发过程中,要尽量减少内存的申请,申请的内存尽量保持连续,对于资源加载应该优先加载较大的资源。
Editor Asset
Unity中的资源通常是指3D模型、音频文件、图像等用于程序使用的数据(代码数据也是一种资源)。程序开发阶段,将外部资源导入或复制到Asset目录下,Unity会监听到所导入的数据,并生成相应的Unity默认资源。
创建Asset
Unity检测到新资源(文件夹)时会向此资源分配GUID,这是Unity在内部使用的ID,用于索引资源,以便Unity移动或重命名此资源不会破坏引用关系。
Unity对导入的外部文件格式不会进行修改,对内置格式资源(prefab/material/unity等),Unity以YAML格式进行存储。
监听资源导入
Unity提供了用于监听资源导入的基类,方便开发者对资源做标准化处理。
资源处理
Unity在导入资源后,会在Library文件夹下生成真正用于Unity处理的资源。例如,Unity 将 .png 图像文件导入为纹理时,在运行时不会使用原始的 .png 格式数据,而是创建新的图形格式,将其存储在项目的Library文件夹中。
Running Time Asset
Unity提供了两种运行时资源:StreamingAssets和AssetBundles(Addressable也是对AssetBundles的封装)。
无压缩资源:StreamingAssets
对于StreamingAssets文件夹下的资源,Unity在创建应用程序时,会直接保留。在目标平台中,此文件夹为只读模式,通常只会存放本地资源包或其它二进制数据(.dll 和脚本文件不参与脚本编译)。
打包资源:Resources
在发布工程时,会将所有Resources文件夹一起打包。同样的在运行时使用Resources加载资源,也需要将整个Resources包体加载。虽然Resources使用十分便捷,但其弊端十分明显。首先其无法对资源进行粒度划分,并且难以支持外部的资源。
打包资源:AssetBundles
AssetBundles可以根据需求,对不同目录进行打包管理。因此在资源更新、包体加载、资源粒度划分等问题有更好的表现。
打包策略
在拆分包体时,需要考虑包体大小、依赖关系、变动频率等方面进行综合考虑,需要开发者根据项目情况进行处理。
官方给了一些实际处理方案:
- 逻辑实体分组:通常是指根据模块功能分组,例如用户界面、角色的模型和动画、共享资源等。
- 类型分组:通常是针对不同的发布版本进行分组。例如不同地区需要相应的语言包、不同平台需要不同的Shader。
- 并发内容分组:将需要同时加载和使用的资源捆绑在一起。常见于场景资源的打包。
依赖加载
为了解决资源冗余问题,当多个包体使用了相同资源时,最佳的处理方案是将这部分资源单独打包。
因此在加载资源时,也需要注意资源的加载顺序。需要先加载被依赖的包体,再加载当前包体,否则会出现引用丢失。
AssetBundles结构
在目标目录中每个包体对应一个资源数据文件以及manifest文件。资源数据文件时Unity对资源压缩加密后的二进制文件,同时还会有一个总的AssetBundles文件。
AssetBundles.manifest记录了所有包体列表以及其依赖关系,单个manifest记录了包体文件列表、包体依赖关系和TypeTree(可选)。由于ui使用了image中的图片资源,因此可以看到其对image包体的依赖。
资源收集与打包
调用BuildPipeline.BuildAssetBundles接口,Unity会自动收集有AssetBundle标签的资源以及其依赖关系,并根据标签名称在目标位置生成包体。也可以指定文件打包。
为了缩短打包时间,Unity在5.0之后开放了增量式打包,会在原有包体基础上,比较manifest文件判断包体数据是否变化。Model文件夹添加一个模型后,再次打包,发现只有AssetBundles和model发生了变化。
默认情况下,Unity 通过 LZMA 压缩来创建 AssetBundle,然后通过 LZ4 压缩将其缓存。LZMA压缩后获得的是 AssetBundle数据流,如果需要从中加载某个资源,就必须将整个流文件解压缩。解压AssetBundle后,将使用 LZ4压缩再次生成资源,使得加载资源时不比加载整个AssetBundle。
一般本地AB建议以LZ4格式存储,对内存友好。远端AB用LZMA压缩,减少数据传输量。
性能检测
Unity性能检测工具
Unity Editor提供了4项性能检测工具:Unity Profiler、Profiling Core package、Memory Profiler、Profile Analyzer。
性能优化流程
(1)确定测试场景
对整个游戏流程进行性能测试几乎无法实现,只能针对具体问题进行解决。因此就需要确定测试的硬件设备;测试的操作流程。
对于压力测试等情况,可以设计特定的场景;对于整体的应用测试来,只能分场景或功能模块进行测试。在测试过程中,可能出现动态内存问题(随着操作,内存增加)与静态内存问题(无操作情况下,内存增加),需要考虑到这两种情况进行处理。
(2)收集数据
利用上述的性能检测工具,收集程序运行数据。通常会对比不同帧数据,根据内存分布情况,定位是哪些资源过大或者操作引起数据剧烈变化。
(3)定位问题与优化
在发现外部测试指标问题后,就可以大致定位到产生瓶颈的过程,但在工程较为复杂的情况下,想精确定位问题也不会很容易。能精确定位问题最为关键,后续团队就可以针对性得优化。