资源内存管理和自动打包-Addressable

资源读取方式

  • Resources
  • AssetBundle
  • Addessable Asset System

Resources

Unity自动管理依赖的AssetBundle,实质是缺省的AssetBundle,只是隐藏了属性。

  • 缺点
  • 无法自定义Bundle的分包,导致打包效率低下和无法细分Bundle优化内存读取
  • 无法热更新
  • 无法分离obb和apk
  • 优点
  • 傻瓜式方便快捷开发

AssetBundle

AssetBundle是Unity提供的一种用来存储资源的文件格式,它可以存储任意一种Unity引擎能够识别的资源,如Scene、Mesh、Material、Texture、Audio、noxss等等,同时,AssetBundle也可以包含开发者自定义的二进制文件,只需要将自定义文件的扩展名改为.bytes,Unity就可以把它识别为TextAsset,进而就可以被打包到AssetBundle中。Unity引擎所能识别的资源我们称为Asset,AssetBundle就是Asset的一个集合。

  • 缺点
  • 需要各种自定义设置参数
  • 复杂的依赖关系
  • 不同平台需要不同适配重新打包
  • 管理复杂的资源关系和内存管理
  • 优点
  • 可以自定义分包Bundle,有利于资源读取和打包速度
  • 热更新OK
  • obb和apk分离OK

概念区分

  • AssetBundle文件
  • AssetBundle镜像
  • Asset
  • 引用对象
  • 依赖关系

使用流程

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




CTYPES 读取 内存 不完整 无法读取内存 vs_加载


AssetBundle读取和释放

  1. new WWW与WWW.LoadFromCacheOrDownload或者CreateFromFile读取Bundle
  2. AssetBundle.Load读取Asset
  3. 使用Asset
  4. AssetBundle.Unload(false),Asset还存在内存中,但是存Asset的AssetBundle镜像没了,AssetBundle.Unload(true) 同时释放Asset和AssetBundle镜像
  5. 如果AssetBundle不想释放镜像,则需要自己注意管理内存, 引用关系和对象null都是需要注意的,AssetBundle加载的asset一样可以用Resources.UnloadUnusedAssets卸载,Resources.UnloadAsset(obj)同理;
  6. GC.Collect()立即释放内存

坑点

  • CreateFromFile(注意这种方法只能用于standalone程序)这是最快的加载方法 未解压
  • AssetBundle三种压缩方式LZMA格式、LZ4格式、不压缩,压缩率下降,读取速度上升
  • new WWW与WWW.LoadFromCacheOrDownload的优劣,运行开销降序,缓存升序。
  • AssetBundle.Unload()中传参false、true区别
  • Resources.Load和静态引用是会把所有资源都预先加载的,反复测试的结果,静态引用和Resources.Load也是OnDemand的,用到时才会加载,所以第一次instantiate时会lag
  • 在合适的地方调用Resources.UnloadUnusedAssets,释放已经没有引用的Asset
  • AssetBundle文件自身的内存镜像,必须要用Unload来释放,这种数据缓存是非托管的
  • CreateFromFile:这种方式不会把整个硬盘AssetBundle文件都加载到 内存来,而是类似建立一个文件操作句柄和缓冲区,需要时才实时Load,所以这种加载方式是最节省资源的,基本上AssetBundle本身不占什么内存,只需要Asset对象的内存。可惜只能在PC/Mac Standalone程序中使用。
  • CreateFromMemory和www.assetBundle:这两种方式AssetBundle文件会整个镜像于内存中,理论上文件多大就需要多大的内存,之后Load时还要占用额外内存去生成Asset对象。

内存

  • 程序代码(Library)
  • 托管堆(Managed Heap)
  • 本地堆(Native Heap)
  • 非托管堆栈(Stack Heap)

概念区分

  • 堆和栈(Heap Stack)
  • 栈就像数据结构中的stack,当变量存储在栈上,内存从栈的末端分配,当变量不在作用域时,内存就立即返回到栈中继续被重用;栈内存的分配和回收十分便捷,栈的结构是后进先出LIFO,只能使用栈顶的数据,想要读取某个元素就要将之前的元素全部出栈,才能完成
  • 堆的结构是无序的表,托管堆内存的分配和回收经常会牵扯托管堆上不同的内存块


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_02


  • 值类型和引用类型
  • 值类型。在C#中,所有从System.ValueType继承的类型
  • 引用类型。类、接口、委托、object对象、string、stringBuilder
  • 大多数值类型的内存会分配到栈上(不受托管),引用类型在堆上,除了少部分类等引用类型内部定义的值类型
  • 栈申请内存快,但不受控制,堆申请慢,由于内存offset结构容易产生碎片,但用起来方便
  • 栈自我管理内存没问题,堆需要GC管理

程序代码

  • 程序代码包括了所有的Unity引擎,使用的库,以及你所写的所有的游戏代码。在编译后,得到的运行文件将会被加载到设备中执行,并占用一定内存。
  • 优化
  • 减少引用库数量和大小
  • .net 2.0 subset
  • use micro mscorlib

托管堆

  • 托管堆是被Mono使用的一部分内存。
  • mono项目一个开源的.net框架的一种实现,对于Unity开发,其实充当了基本类库的角色。托管堆用来存放类的实例(比如用new生成的列表,实例中的各种声明的变量等)。“托管”的意思是mono“应该”自动地改变堆的大小来适应你所需要的内存, 并且定时地使用垃圾回收(Garbage Collect)来释放已经不需要的内存。关键在于,有时候你会忘记清除对已经不需要再使用的内存的引用,
  • unity的mono堆内存分配后不会返还给系统,目前Unity所使用的mono版本存在一个很严重的问题,Mono的堆内存是只升不降。


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_03


本地堆

  • 本机堆是Unity引擎进行申请和操作的地方,比如贴图,音效,关卡数据等。Unity使用了自己的一套内存管理机制来使这块内存具有和托管堆类似的功能
  • 优化
  • Hierarchy尽量不引用Asset资源

非托管堆栈

  • 非托管栈(Stack),由项目脚本运行时的内存管理器(Mono或IL2CPP)管理,是函数内的值类型或者结构以及结构里定义的值类型等。大多数值类型都是在非托管栈内,除了少部分类等引用类型内部定义的值类型。
  • 非托管堆(Heap), 部分Unity内部管理的非托管引用对象,如AssetBundle,无法通过被垃圾回收器访问、管理,需要手工管理内存释放的堆。

坑点

  • 不要在频繁调用的函数中反复进行堆内存分配(Update)
    在像Update()和LateUpdate()这种每帧都调用的函数,可以判断值变化了才调用某个会有堆内存分配的函数,或者计时器到了才调用某个函数
  • 清空而不是创建集合(Pool)
    创建新的集合(比如:数组,字典,链表等集合类数据)会导致托管堆上的内存分配 , 如果发现在代码中不止一次地创建新集合,那么我们应该缓存引用到的集合,并使用Clear()清空其内容,而不是重复调用new()
  • 尽可能避免C#中的闭包
    在性能敏感的代码中应该尽可能的减少匿名方法和方法引用的使用,尤其是在基于每帧执行的代码中。 匿名方法要求该方法能够访问方法范围之外的变量状态,因此已成为闭包。于是C#通过生成一个匿名类,可以保留闭包所需的外部范围变量。 当执行闭包需要实例化其生成的类的副本,并且所有类都是C#中的引用类型,所以执行闭包需要在托管堆上分配对象。
  • 装箱
    装箱是Unity中非常常见的非预期的临时内存分配原因之一。只要将值类型值用作引用类型,就会发生这种情况
  • string相关
    在C#中,string字符串是引用类型而不是值类型。C#中的字符串是不可变更的,其引用指向的值在创建后是不可被变更的。因此在创建或者丢弃字符串的时候,会造成托管堆内存分配。 推荐做法: 减少不必要的字符串创建,提前创建并持有缓存 减少不必要的字符串操作,比如常用的+。每次在对字符串进行操作的时候(例如运用字符串的”+”操作),unity会新建一个字符串用来存储相加后的字符串。然后使之前的旧字符串被标记为废弃,成为内存垃圾。 改用StringBuilder类 , StringBuilder就是专门设计用来创建字符串而不产生额外托管堆分配的类,而且可以避免字符串拼接产生垃圾
  • 注意由于调用Unity的API所造成的堆内存分配
    如果函数需要返回一个数组,则一个新的数组会被创建用作结果返回,简单地缓存一个对数组的引用
  • 部分函数的CG陷阱
    函数gameobject.name或gameobject.tag,可以用CompareTo(),用Input.GetTouch()和Input.touchCount()来代替Input.touches或者用Physics.SphereCastNonAlloc()来代替Physics.SphereCastAll()

Addressable Asset System

Unity2018.2之后提供了一个新的资源管理和打包功能, Addressable Asset System。可寻址资源管理系统,这个系统用来替代之前的AssetBundle, 提供了一系列的方法来为开发者部署游戏资源提供便利。

  • 缺点
  • preview阶段,文档缺少
  • 不完善,无法同步读取,替换成本大
  • 优点
  • 依赖关系
  • 内存管理
  • 打包效率,取决于包体区分以及颗粒度

概念区分

  • Address:每个资源都有一个唯一的标志地址


CTYPES 读取 内存 不完整 无法读取内存 vs_字符串_04


  • AddressableAssetData directory:存放打包资源的位置


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_05


  • Asset group:资源组,便于打包分块


CTYPES 读取 内存 不完整 无法读取内存 vs_加载_06


  • Asset group schema:资源组的打包设置


CTYPES 读取 内存 不完整 无法读取内存 vs_值类型_07


  • AssetReference:对资源的引用,只有在使用的时候才会加载


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_08


  • Asynchronous loading:异步加载
  • Build script:打包设置对应不同脚本


CTYPES 读取 内存 不完整 无法读取内存 vs_加载_09


  • PlayMode script:编辑器下资源加载读取的不同途径


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_10


CTYPES 读取 内存 不完整 无法读取内存 vs_字符串_11


  • Label:资源标签,用于某类资源的共同读取,如Addressables.DownloadDependenciesAsync("spaceHazards");


CTYPES 读取 内存 不完整 无法读取内存 vs_字符串_12


问题:

  1. 游戏运行中读取资源是否可以不管理依赖关系,内存管理问题?哪个更快,体积更小,CPU占有率更低?
  2. 资源分组是否能提高打包效率?

游戏运行中读取资源是否可以不管理依赖关系,内存管理问题?哪个更快,体积更小,CPU占有率更低?

黑盒测试:

448个快1GB的小黄漫和1个夏喵表情包


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_13


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_14


AssetBundle和AAS分别将xiamiao_texture和黄漫放在同一个资源组下

  • AssetBundle:都放在textures组下


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_15


  • AAS:都放在Textures的Asset group下


CTYPES 读取 内存 不完整 无法读取内存 vs_值类型_16


打包后

大小比较

  • AAS(我相信是bug):


CTYPES 读取 内存 不完整 无法读取内存 vs_加载_17


  • AssetBundle:


CTYPES 读取 内存 不完整 无法读取内存 vs_加载_18


打包快慢比较

  • AAS:184.16s
  • AssetBundle:187.77s

读取加载


//AAS读取
public void ASLoad()
{
    ResourcesManager.instance.LoadFromNameAsync<Texture2D>("xiamiao_texture", tex =>
    {
        Instantiate(tex, new Vector3(-5f, 0f, 0f));
    });
}

//AssetBundle读取
public void ABLoad()
{
    var ab = AssetBundle.LoadFromFile($"{Application.streamingAssetsPath}/AssetBundles/textures");
    var handle = ab.LoadAssetAsync<Texture2D>("xiamiao_texture");
    CoroutineManager.instance.StartCoroutine(() =>
    {
        Instantiate(handle.asset as Texture2D, new Vector3(5f, 0f, 0f));
    }, () => handle.isDone);
    
}

private GameObject Instantiate(Texture2D tex, Vector3 pos)
{
    var sprite = GameUtils.CreateSpriteFromTexture(tex);
    var go = new GameObject();
    var sr = go.AddComponent<SpriteRenderer>();
    sr.sprite = sprite;
    go.transform.position = pos;
    return go;
}


CTYPES 读取 内存 不完整 无法读取内存 vs_CTYPES 读取 内存 不完整_19


Load AS


CTYPES 读取 内存 不完整 无法读取内存 vs_值类型_20


CTYPES 读取 内存 不完整 无法读取内存 vs_值类型_21


Load AB(CreateFromFile)


CTYPES 读取 内存 不完整 无法读取内存 vs_值类型_22


CTYPES 读取 内存 不完整 无法读取内存 vs_CTYPES 读取 内存 不完整_23


Load AB(WWW)


CTYPES 读取 内存 不完整 无法读取内存 vs_值类型_24


结论

  1. AAS读取资源可以不管理依赖关系,占用内存更小,其次为LoadFromFile,最大为WWW
  2. 打包时间不相上下
  3. AAS读取花了5s,AssetBundle(LoadFromFile和WWW)大约32s,
  4. 包体大小变大?貌似是AAS的bug
  5. 同样异步读取,AAS最高为21%,AssetBundle的WWW最高为12.9%,CreateFromFile为34.6%

资源分组是否能提高打包效率?

  1. 清除打包缓存
  2. 分两个Asset group,一个Textures_0有一个夏喵表情包和近1G的小黄漫,另一个只有一香蕉喵和近70M的小黄漫


CTYPES 读取 内存 不完整 无法读取内存 vs_值类型_25


3. 打包,193s左右

4. 改香蕉喵的图片设置


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_26


5. 再次打包,27s左右


结论

打包以Asset group为组进行资源分包,可以通过合理的分组优化打包效率


依赖关系的打包效率呢?

  1. 两个Asset group:Asset1、Asset2


CTYPES 读取 内存 不完整 无法读取内存 vs_vs无法读取内存_27


2. Asset1:

  • xiamiao_prefab(依赖xiamiao_texture和xiamiao_texture1)
  • xiamiao_texture1
  • 近1G的小黄漫


CTYPES 读取 内存 不完整 无法读取内存 vs_字符串_28


3. Asset2:

  • xiamiao_texture
  • 60M的小黄漫


CTYPES 读取 内存 不完整 无法读取内存 vs_字符串_29


4. 清楚打包缓存

5. 打包,195s

6. 实机


CTYPES 读取 内存 不完整 无法读取内存 vs_加载_30


7. 改动xiamiao_texture设置


CTYPES 读取 内存 不完整 无法读取内存 vs_值类型_31


8. 打包,26s

9. 实机


CTYPES 读取 内存 不完整 无法读取内存 vs_值类型_32



结论

管理好Asset group的依赖关系,也能提高打包效率


Adressable Asset System已经整合到PeroTools2.ResourcesManager里面了,大家快一起用吧~