本文内容主要翻译自下面这篇文章

https://unity3d.com/cn/learn/tutorials/topics/best-practices/guide-assetbundles-and-resources?playlist=30089  A guide to AssetBundles and Resources

为了消除一些歧义,文章里面的专有名词直接用英文单词,比如Assets、Resource、Object

这篇文章是关于在unity引擎中进行Assets和resource管理的深度讨论,以下分四个部分来进行:

  1. 分析unity序列化Assets和处理Assets之间的引用的底层细节。
  2. 讨论内建的Resources API。
  3. 在第一部分的基础上讨论AssetBundle的基础。例如加载AssetsBundles以及从AssetBundles加载Assets。
  4. 讨论AssetBundle的应用模式。比如给AssetBundle赋值Assets以及从AssetBundle加载Assets并讨论开发者在使用AssetBundle遇到的一些常见陷阱。

注:本文说到的词汇Objects和Assets与Unity公开API的命名规范有些不同。比如本文说道Objects在一些API中被称为Assets,比如AssetBundle.LoadAsset和Resources.UnloadUnUsedAssets.本文称为Assets的文件很少有API对外公开,就算公开也是在Build相关的API。比如AssetDataBase和BuildPipeline。在这些API中他们被较为文件(files)。

第一部分:Assets,Objects 和序列化

本部分讨论在编辑器和运行时unity引擎序列化和维护Objects之间的引用关系的内部细节。也讨论了Objects和Assets之间的区别。理解了这些就可以在unity中有效的加卸载Assets。正确的Assets管理是保证较少的加载时间和更少的内存消耗的关键。

1.1  Inside Assets And Objects

要想在unity中正确的管理数据,就要理解unity识别(identifiles)和序列化(serializes)数据。

首先我们来区分Assets和UnityEngine.Objects:

一个Assets就是一个存储在Unity项目中Assets文件夹里面的一个存储文件。比如文理文件、材质文件和FBX文件都是Assets。有些Assets包含unity本地格式的数据。有些Assets需要处理成本地格式。比如FBX文件。

UnityEngine.Object是一套序列化的数据集合,用来描述一个Resource的特殊实例(instance)。Unity引擎可用的Resource可以是各种类型,比如网格、精灵、声音片段、动画片段等。所有的Objects都是UnityEngine.Object的子类。虽然大部分Objects类型都是内建的。但是有两个特殊的类型:

  1. ScriptableObject给开发者提供了便于自定义自己数据类型的方法。这个类型能够被unity序列化和反序列化,并且能够在检视窗口编辑。
  2. MonoBehaviour提供了一个连接到MomoScript的包装。一个MonoScript是一个内部数据类型,unity可以用他来保持到一个程序集和命名空间中某个脚本类的引用。

Assets和Objects是一对多的联系,一个给定的Asset文件包含一个或多个Objects。

1.2  Object之间的引用

所有的UnitEngine.Objects都能引起其他的Objects。别的Objects既可能存在于相同的Asset文件里面也可以从别的Asset文件里面导入。比如一个材质Object经常有一个或者多个贴图Objects的引用。这些贴图Objects一般是从一个或多个贴图Asset文件导入的。

当序列化时,这些引用包含两部分数据:文件的GUID和本地ID(local ID)。文件GUID用来确定Asset文件存储位置。本地ID用来确定Asset文件里面每个Object,因为每个Asset文件可能包含多个Objects。

文件GUID存储在.meta文件里面。当Unity首次导入一个Asset时候unity在相同的目录下为该Asset生成一个.meta文件。

我们可以用文本编辑器看到上面说的情况。新建一个unity项目,在Editor设置里面显示Meta文件并设置序列化Asset为文本格式。在项目中导入一张贴图,并创建一个材质,把贴图赋给材质。在场景中创建一个立方体,并把材质赋予该立方体。然后用文本编辑器打开材质的.meta,就可以看到在顶部有一行标记着guid。这就定义着该Asset文件的GUID。用文本编辑器打开材质文件,就可以找到局部id,比如看起来像下面这样的

---!u!21 & 2100000

Material:

serializedVersion: 3

... more data ...

上面2100000就是该Materail的本地ID。如果该Materail Object的所属Asset文件的GUID是“abcedfg”,那么该Material Object就可以被GUID“abcdefg” 和本地ID “2100000”唯一标记。

1.3  为什么需要File GUID和本地ID?

为什么GUID和本地ID是必须的呢?答案就是为了健壮性和弹性。

GUID抽象了文件位置。只要一个GUID能够和一个文件联系起来,文件位置就变得不相关了,所以在Unity里面文件可以自由的移动而不用更新引用了这个文件的Objects。

因为一个Asset文件可以包含多个Object资源,所以本地ID需要用来区分每个独立的Object。

如果Asset文件的GUID丢失了,那么所有的引用该Asset文件的Objects也就丢失了。所以.meta文件同Asset文件保存在同一个目录非常重要。Unity会重新生成删除掉或者放错位置的meta文件。

Unity编辑器维护一个一个路径同GUID映射集合。当一个Asset被载入项目的时候,就新增加一个映射。如果Editor在运行状态时发现一个meta文件丢失,只要Asset路径没有改变。Unity会确保生成相同的GUID。

如果unity没有运行而meta文件丢失,或者Asset路径改变了而meta文件没有一同移走。所有引用该Asset里面的Objects都会被丢失。

1.4  composite Assets and importers

前面提到非本地Asset类型可以导入到unity,这是通过asset导入器来完成的。虽然通常是自动调用的,但是仍然可以通过AssetImporterApi来调用。比如导入纹理Asset时,TextureImporterApi对外提供设置方法。

导入处理的结果就是一个或者多个Objects。这些在UnityEditor里面已父Asset里面的子资源的形式对外可见。比如一张贴图以精灵图集导入的时候,下面有多个精灵。每个Object共享一个File GUID,因为他们存储在同一个Asset文件里面,精灵之间以本地ID进行区分。

导入处理会把源Asset处理成Editor设置的目标平台适用格式。这种处理会保护非常重的操作,比如文理压缩。如果Editor每次打开都要操作的话效率将非常低下。因此unity会把处理结果缓存在Library目录。特别的是存在Library/metadata/目录中以GUID前面2位组成的目录里面。

1.5  序列化和实例化

虽然GUID和本地ID是健壮的,但是GUID比较比较慢因此运行时需要优化。Unity内部维护一个缓存,把GUID和LocalID转换成一个唯一的整数,被称为实例ID。该缓存维护这实例ID和GUID和本地ID定义的源位置以及内存里面的Object(如果有)。这就使得Object能够维持互相引用。通过实例ID能够快速查找已经加载的Object。如果目标Object还没有加载,通过GUID和本地ID,可以定位Asset位置,unity就可以及时Load该Object。

一开始,实例ID缓存会初始化所有的项目需要编译的Objects(比如场景引用的),以及Resource目录所有的Objects。运行时新导入的资源会附加进来以及从AssetBundles加载的Objects。实例ID的条目只有在没用的时候才从缓存移走。比如AsseBundle被卸载了。

1.6  MonoScripts

认识到MonoBehaviour有到MonoScript的引用非常重要。MonoScript仅仅是包含一些定位一个脚本的信息并不包含类的可执行代码:程序集名称、类名称和命名空间。

当编译一个项目时候,unity收集所有的脚本并编译到一个程序集。Unity会为每一种语言编一个程序集,同时也为Plugins目录编译一个程序集。比如plugins目录外的C#脚本编译成 Assembly-Csharp.dll。plugin目录下的编译成Assembly-Csharp-firstpass.dll。

程序集会被包含进最终的应用程序里面。MonoScript就是指向这里的引用。当程序开始运行时,所有的程序集都被加载。

这就是为什么AssetBundle没有真正包含可执行代码的原因。

1.7  资源生命周期

Objects在内存中加载和卸载。为了减少加载时间和管理应用内存,我们需要理解他的资源的生命周期。

有自动和显式加载Object。当实例ID和Object没有关联到时表面Object没有没加载到内存,当可以定位到Asset,Unity就自动加载Object。脚本可以显式加载Object,比如既可以创建他们也可以通过资源加载API(AssetBundle.LoadAsset).

当被加载后,unity会把GUID和本地ID的引用解析成实例ID。

一个Object的实例ID被第一次引用到时满足下面两个条件就会加载:

  1. 实例ID引用的Object当前还没有被加载。
  2. 实例ID关联的GUID和本地ID已在缓存中注册。

如果GUID和本地ID没有实例ID,或者实例ID引用一个错误的GUID和本地ID,引用被保留了,但是Object却不能加载。此时会出现一个Missing。丢失的Object根据类型是可见的,比如纹理丢失的话就会出现洋红色。

Objects的在以下情况会被卸载:

  1. 当清理未被使用的Asset时Objects自动被卸载。比如场景切换或者代码调用Resource.UnloadUnusedAssets方法时就会触发清理资源。这只会卸载没有被引用的Objects:既没有脚本变量引用到这个Object,也没有其他活跃状态的Objects引用这个Object.
  2. 从Resource文件夹加载的对象能通过Resource.UnloadAsset方法显式卸载。卸载后该Object实例ID仍在,如果卸载后有脚本变量或者其他对象有引用,这个Object会被立刻重新加载。
  3. 调用AssetBundle.UnLoad(true)时从AssetBundle加载的Objects会被立即卸载。任何引用该Object都会显示丢失。脚本里面任何引用卸载对象都会引发空引用错误。

如果AssetBundle.UnLoad(false)调用后,从AssetBundle加载的Objects会被被销毁掉。但是Unity会是GUID和本地ID同实例ID的关联失效。如果这些遗留的Object被卸载后,就不能重新再被加载了。

1.8  加载深层级对象

当序列化层级Objects(比如预制对象)时,整个层级都会被完整的序列化。也即每一个对象和组件都被单个的序列化到数据里面去,这也会影响加载和实例化时间 。

当创建GameObject层级时,CPU时间主要花费在下面几个方面:

  1. 读取资源数据(从存储器或者别的GameObject)
  2. 设置新的Transform的父子关系
  3. 实例新的GameObject和组件
  4. 唤醒GameObject和组件

无论是克隆还是从存储器读取,后面三部花费时间基本不变,但是读取时间却随着层级增加线性增加。

现在平台上,从内存要不存储设备要快。将来随着存储介质的变化,

能也会有很大的不同,桌面pc就要把移动设备要快。如果从慢设备上面加载。读取数据时间就大大超过实例化时间,因此性能瓶颈就在I/O上面。

当序列化一个预制对象时,所有的GameObject和组件数据都要序列化

即使是复制的对象。比如一个屏幕UI有30个一样的元素,这30个一样的元素就要序列化30次。会产生大量的二级制数据。加载的时候,这些数据又要从磁盘读取并被转换成新实例化的Object。造成实例化大型预制对象时文件读取占据主要时间消耗。

一旦被实例化后,克隆一个则比从磁盘读取加载要快得多。

注:unity5.4修改了transform对象在内存中的表示,每个根transform的子transform在内存中都是紧凑排列的。当需要实例一个立刻为其他对象子对象的GameObject时,可以考虑用新的GameObject.Instantiate重载方法,该方法接受一个parent参数,速度回提升5-10个百分点。

Unity AssetBundles and Resources指引 (一)的更多相关文章

  1. Unity AssetBundles and Resources指引 (四) AssetBundle使用模式

    本文内容主要翻译自下面这篇文章 https://unity3d.com/cn/learn/tutorials/topics/best-practices/guide-assetbundles-and- ...

  2. Unity AssetBundles and Resources指引 (三) AssetBundle基础

    本文内容主要翻译自下面这篇文章 https://unity3d.com/cn/learn/tutorials/topics/best-practices/guide-assetbundles-and- ...

  3. Unity AssetBundles and Resources指引 (二) Resources文件夹

    本文内容主要翻译自下面这篇文章 https://unity3d.com/cn/learn/tutorials/topics/best-practices/guide-assetbundles-and- ...

  4. 【Unity】使用Resources类管理资源

    最近参考了各位大神的资源,初步学习了Unity的资源管理模式,包括在编辑器管理(使用AssetDatabase)和在运行时管理(使用Resources和AssetBundle).在此简单总结运行时用R ...

  5. Unity Android 5.6版本Resources.Load效率的问题

    0x00 前言 相信不少使用Unity的小伙伴都听说过,甚至也亲身经历过在Unity5.6最初的几个版本中使用Resources.Load方法加载资源变--慢的问题. 这个问题的确是存在的,比如这个i ...

  6. 【Unity游戏开发】AssetBundle杂记--AssetBundle的二三事

    一.简介 马三在公司大部分时间做的都是游戏业务逻辑和编辑器工具等相关工作,因此对Unity AssetBundle这块的知识点并不是很熟悉,自己也是有打算想了解并熟悉一下AssetBundle,掌握一 ...

  7. Unity 官方教程 学习

    Interface & Essentials Using the Unity Interface 1.Interface Overview https://unity3d.com/cn/lea ...

  8. unity开发相关环境(vs、MonoDevelop)windows平台编码问题

    情景描述:最近在做Unity的网络底层,用VS编写源码,MonoDevelop用来Debug,在Flash Builder上搭建的Python做协议生成器,期间有无数次Unity莫名奇妙的的down掉 ...

  9. (转)unity开发相关环境(vs、MonoDevelop)windows平台编码问题

    转自: http://www.cnblogs.com/sevenyuan/archive/2012/12/06/2805114.html 1.unity会爆出错误: There are inconsi ...

随机推荐

  1. Neutron LBaaS Service(2)—— Neutron Services Insertion Model

    Service Insertion Service Insertion是Neutron中实现L4/L7层服务的框架.Neutron以前只有一级插件结构用于实现各种L2层技术(如LinuxBridge, ...

  2. 剑指offer系列29-----链表中环的入口节点-

    [题目]一个链表中包含环,请找出该链表的环的入口结点. [思路]方法一:使用双指针 方法二:利用set集合的特性,不能添加重复数字,否则返回false package com.exe7.offer; ...

  3. 读书笔记:应用随机过程:概率模型导论:Aloha协议问题

    例4.16,Aloha协议:就本书例题所涉及的部分来说,几乎等同于CSMA.这个例题重写如下: 考察一个包含多个设备的通信系统,其中在每个时间段发送信息的设备个数是独立同分布的.......每个设备将 ...

  4. Redis集群方案介绍

    由于Redis出众的性能,其在众多的移动互联网企业中得到广泛的应用.Redis在3.0版本前只支持单实例模式,虽然现在的服务器内存可以到100GB.200GB的规模,但是单实例模式限制了Redis没法 ...

  5. CGI技术原理

    一.CGI技术 1.1 CGI的提出 CGI是外部扩展应用程序与WWW服务器交互的一个标准接口.按照CGI标准编写的外部扩展应用程序可以处理客户端(一般是WWW浏览器)输入的协同工作数据,完成客户端与 ...

  6. 黄聪:在WordPress后台文章编辑器的上方或下方添加提示内容

    WordPress 3.5 新增了一对非常有用的挂钩,可以快速在WordPress后台文章编辑器的上方或下方添加提示内容,下面是一个简单的例子,直接将代码添加到主题的 functions.php 文件 ...

  7. CLR和JIT

    在使用IDE进行编译的时候,这个过程具体的叫法是,使用编译器面向CLR来生成代码.对于不同的开发语言,使用的的编译器也不一样,但是生成的代码都一样. “无论选用哪一个编译器,结果都是一个托管模块.” ...

  8. Java设计模式—生产者消费者模式(阻塞队列实现)

    生产者消费者模式是并发.多线程编程中经典的设计模式,生产者和消费者通过分离的执行工作解耦,简化了开发模式,生产者和消费者可以以不同的速度生产和消费数据.这篇文章我们来看看什么是生产者消费者模式,这个问 ...

  9. JDBC常用代码

    try { //加载驱动 Class.forName("com.mysql.jdbc.Driver"); String url="jdbc:mysql://127.0.0 ...

  10. ruby 字符串学习笔记3

    ascii转字符或者字符串转ascii "a".ord # => 97 "!".ord # => 33 "\n".ord # = ...