Asset详解
Asset资源映射
什么是Asset
Assets目录
创建一个新项目时,会有一些默认目录
- Assets 实际资源目录
-
Library 存放Unity处理完毕的资源,大部分的资源导入到Assets目录之后,还需要通过Unity转化成Unity认可的文件,转化后的文件会存储在这个目录
-
Packages 2018以后新增的目录,用于管理Unity分离的packages组件
- ProjectSettings 存放Unity的各种项目设定
AssetBundles
等同于ZIP或者RAR等压缩文件,但是只针对于Unity的Asset,拥有平台差异性,且不包含代码
Unity Asset
也就是Unity能够识别的文件,一种是Unity原生支持的文件,比如材质球。一种是需要特殊处理后才能支持的文件,比如FBX。对于后者,Unity提供了可以自定义的脚本Import
Asset的识别和引用
Assets和Objects
Asset指Project面板里可以看到的所有文件,包含单个文件以及文件夹
Object指继承自UnityEngine.Object的对象,一个可以序列化的数据
大多数Object都是内置支持的,除了:
-
ScriptableObject 提供了一个指向MonoScript的转换器。MonoScript是一个Unity内部的数据类型,它不是可执行代码,但是会在特定的命名空间和程序集
下,保持对某个或者特殊脚本的引用。
-
MonoBehaviour 提供了一个指向MonoScript的转换器。MonoScript是一个Unity内部的数据类型,它不是可执行代码,但是会在特定的命名空间和程序集
下,保持对某个或者特殊脚本的引用。
Asset与Object是1对N的关系,一个Prefab可以作为Asset,可以包含很多的Object
File GUIDs和Local IDs
Unity把序列化分为两个部分
第一部分叫做File GUID,标识这个资产的位置,这个GUID是由Unity根据内部算法自动生成的,并且存放在原始文件的同目录、同名但是后缀为.meta的文件里
- 第一次导入资源的时候Unity会自动生成
- 在Unity的面板里移动位置,Unity会自动帮你同步.meta文件
- 在Unity打开的情况下,单独删除.meta,Unity可以确保重新生成的GUID和现有的一样
- 在Unity关闭的情况下,移动或者删除.meta文件,Unity无法恢复到原有的GUID,也就是说引用会丢失
第二部分叫做Local ID,表示当前的Object在Asset里的唯一标识
File GUID确保了资产在整个Unity工程里唯一,Local ID确保Objects在资产里唯一,这样就可以通过二者的组合去快速找到对应的引用
Unity还在内部维护了一张资产GUID和路径的映射表,每当有新的资源进入工程,或者删除了某些资源,又或者调整了资源路径,Unity的编辑器都会自动修改这
张映射表以便正确地记录资产位置
所以如果.meta文件丢失或者重新生成了不一样的GUID,Unity就会丢失引用
Library中的资源位置
Unity不支持的资源格式,会通过Import进行资源转换,所有的转换结果都会存储在Library/metadata/目录下,以File GUID的前两位进行命名的文件夹里
Instance ID
运行时所采用的ID,与File ID和Local ID呈映射关系
只要系统解析到一个Instance ID,就能快速找到代表这个Instance ID的已加载的对象。如果Object没有被加载,File GUID和Local ID也可以快速地定位到指定的
Asset资源从而即时进行资源加载
资源生命周期
加载
当Unity启动时,缓存系统会对项目立刻要用到的数据(比如:启动场景里的这些或者它的依赖项),以及所有包含在Resources目录的Objects进行初始化
如果在运行时导入了Asset或者从AssetBundles(比如:远程下载下来的)加载Object都会产生新的Instance ID
另外Object在满足下列条件的情况时会自动加载,比如:
- 某个Object的Instance ID被间接引用了
- Object当前没有被加载进内存
- 可以定位到Object的源位置(File GUID 和 Local ID)
卸载
-
当没有使用的Asset在执行清理的时候,会自动卸载对应的Object。一般是由切场景或者手动调用了Resources.UnloadUnusedAssets的API的时候触发的。但
是这个过程只会卸载那些没有任何引用的Objects
-
从Resources目录下加载的Objects可以通过调用Resources.UnloadAsset进行显式的卸载。但这些Objects的Instance ID会保持有效,并且仍然会包含有
效的FileGUID 和LocalID。这个Object在被直接或者间接引用之后会马上被加载。
-
从AssetBundles里得到的Objects在执行了AssetBundle.Unload(true)之后,会立刻自动的被卸载,并且这会立刻让这些Objects的File GUID、Local ID以
及Instance ID立马失效。任何试图访问它的操作都会触发一个NullReferenceException。但如果调用的是AssetBundle.Unload(false)API,那么生命周期内的
Objects不会随着AssetBundle一起被销毁,但是Unity会中断File GUID 、Local ID和对应Instance IDs之间的联系,也就是说,如果这些Objects在未
来的某些时候被销毁了,那么当再次对这些Objects进行引用的时候,是没法再自动进行重加载的。如果再次加载相同Asset,Unity也不会复用先前加载的
Objects,而是会重新创建Instance ID,也就是说内存里会有多份冗余的资源。
Resources目录的优点与痛点
特殊的工程目录
Editor
这个目录是用来辅助开发的,可以是一个,也可以有很多个,可以在任意的子目录下面。主要作用就是可以编译Unity编辑器模式下提供的脚本和接口,用于辅助
研发,创建各种资源检查,生成工具,以及自定义Unity的工具栏,窗口栏等等。Editor目录下的所有脚本都不会被编译到正式发行包里,很多很多优秀的插件都是
需要通过Unity提供的编辑器下的扩展接口来实现功能扩展的。
Editor Default Resources
这个需要和Editor配合使用。一个扩展面板或者工具光秃秃的也不好看,配上一些美化资源之后会让你的插件或者工具格局更高。要注意的是,这个目录只能是唯
一的,并且只能放在Assets的根目录下
Plugins
这个目录是存放代码之外的库文件的。比如引入的第三方代码,SDK接入的各种jar包,.a文件,.so文件.framwork文件等等,这些库文件会在Unity编译的时候链
接到你的DLL里
Resources
这个和Editor一样,可以在Assets下的任何目录下,并且可以有任意多份。所有Resources目录下的文件都会直接打进一个特殊的Bundles中,并且在游戏启动
时,会生成一个序列化映射表,并加载进内存里。
Gizmos
这个目录其实比较简单,就是可以辅助你在Unity的Scene视窗里显示一些辅助标线,图标或者其它的标识,用来辅助开发定位资源,因为是Scene视窗的,所以
Game视窗和发布之后都不会看到。
StreamingAssets
这个文件是Unity的一个重要文件。Unity发布程序或者游戏,资源随包出去的时候,只有2个地方,一个就是Resources目录,另外一个就是StreamingAssets。这
个目录的资源,文件或者任何东西,都会原封不动的复制到最终的Apk或者iOS的包内。
Resources详解
AB有很多的缺点,比如:
- 无法直观地看到包内的资源情况
- 异步加载,需要写比较繁琐的回调处理
- 调试的时候,无法通过Hierarchy直接定位到资源
- 使用之前需要花费时间进行打包,尤其是在开发的时候,调整资源频繁,如果忘记打包可能导致Bug
Resource的使用相对来说就非常简单了
Resources目录实践
官方建议:不要使用Resouces(摊手)
- Resources内的资源会增加应用程序的启动时间和构建时长
- Resources内的资源无法增量更新,这是现在手机游戏开发的致命点
那些情况可以使用Resources
- 某些资源是项目整个生命周期都必须要用的
- 有些很重要,但是却不怎么占内存的
- 不怎么需要变化,并且不需要进行平台差异化处理的
- 用于系统启动时候最小引导的
开发时的替代方案
AssetDatabase(略)
AssetBundle原理
结构
和传统ZIP没什么区别,由两个部分组成:包头、数据
包头包含有关AssetBundle的信息,比如标识符、压缩类型和内容清单
数据段包含通过序列化AssetBundle中的Assets而生成的原始数据。如果指定LZMA为压缩方案,则是先把所有Asset打包,然后整体压缩。如果指定了
LZ4,则是先压缩每一个Asset,然后一起打包。当然你也可以不压缩。
加载AssetBundle
AssetBundle.LoadFromMemory(Async)
不建议使用这个API
他是从托管代码的字节数组里加载AssetBundle,也就是说你要提前用其它的方式将资源的二进制数组加入到内存中
然后该接口会将源数据从托管代码字节数组复制到新分配的、连续的本机内存块中
但如果AssetBundle使用了LZMA压缩类型,它将在复制时解压AssetBundle。而未压缩和LZ4压缩类型的AssetBundle将逐字节的完整复制
之所以不建议使用该API是因为,此API消耗的最大内存量将至少是AssetBundle的两倍:本机内存中的一个副本,和从托管字节数组中复制的一个副本
AssetBundle.LoadFromFile(Async)
一种高效的API,用于从本地存储加载未压缩或LZ4压缩格式的AssetBundle
在桌面独立平台、控制台和移动平台上,API将只加载AssetBundle的头部,并将剩余的数据留在磁盘上,AssetBundle的Objects会按需加载
但在Editor环境下,API还是会把整个AssetBundle加载到内存中
AssetBundleDownloadHandler
DownloadHandlerAssetBundle的操作是通过UnityWebRequest的API来完成的
使用UnityWebRequest下载AssetBundle的最简单方法是调用UnityWebRequest.GetAssetBundle
LZMA压缩的AssetBundles将在下载和缓存的时候更改为LZ4压缩。这个可以通过设置Caching.CompressionEnable属性来更改
建议
- 只要有可能,就应该使用AssetBundle.LoadFromFile
- 对于必须下载或热更新AssetBundles的项目,建议使用UnityWebRequest
- 当使用UnityWebRequest时,要确保下载程序代码在加载AssetBundle后正确地调用Dispose
- 对于需要独特的、特定的缓存或下载需求的大项目,可以考虑使用自定义的下载器
加载Asset
- LoadAsset (LoadAssetAsync)
- LoadAllAssets (LoadAllAssetsAsync)
- LoadAssetWithSubAssets (LoadAssetWithSubAssetsAsync)
这些API的同步版本总是比异步版本快至少一个帧(其实是因为异步版本为了确保异步,都至少延迟了1帧)
LoadAllAsset比对LoadAsset的多个单独调用略快一些。因此,如果要加载的Asset数量很大,那么使用LoadAllAsset
但如果需要一次性加载不到三分之二的AssetBundle,则要考虑将AssetBundle拆分为多个较小的包,再使用LoadAllAsset
如果需要加载的对象都来自同一Asset,但与许多其它无关对象一起存储在AssetBundle中,则使用LoadAssetWithSubAssets
任何其它情况,请使用LoadAsset或LoadAssetAsync
加载细节
Object加载是在主线程上执行,同步AssetBundle.Load方法将暂停主线程,直到Object加载完成,通过设置Application.backgroundLoadingPriority
可以限制每帧最多的加载时长
- ThreadPriority.High:每帧最多50毫秒
- ThreadPriority.Normal:每帧最多10毫秒
- ThreadPriority.BelowNormal:每帧最多4毫秒
- ThreadPriority.Low:每帧最多2毫秒
AssetBundle之间的依赖
可以通过AssetDatabaseAPI查询AssetBundle依赖项
当某个AssetBundle中的Object引用了其它AssetBundle里的Object时,那么这个AssetBundle就会依赖于另外的AssetBundle
在加载AB时,就会为所有的Object分配好InstanceID,而在某一个InstanceID间接或直接被引用时,对应的Object才会加载
因此哪怕两个AB包之间存在依赖,AB包的加载顺序也不重要,但是一旦你需要加载AB包里指定的资源了,那么需要保证这个资源所依赖的AB已经被加载过了
假设Material A引用Texture B。Material A被打包到AssetBundle1中,Texture B被打包到AssetBundle2中
AssetBundle2必须在Material A从AssetBundle1中加载之前先加载。这并不意味着AssetBundle 2必须在AssetBundle 1之前加载
在将Material A从AssetBundle 1加载之前加载AssetBundle 2就足够了。简单来说就是AssetBundle之间的加载没有先后,但是Asset的加载有
AssetBundle manifests
当使用BuildPiine.BuildAssetBundles API执行AssetBundle构建管线时,Unity会序列化一个包含每个AssetBundle依赖项信息的数据。此数据存储在单独的一个
AssetBundle中,其中包含AssetBundleManifest类型的单个对象
包含Manifest的AssetBundle可以像任何其它AssetBundle一样加载、缓存和卸载
- AssetBundleManifest.GetAllDependencies 返回AssetBundle的所有层次依赖项,其中包括AssetBundle的直接子级、其子级的依赖项等。
- AssetBundleManifest.GetDirectDependations 只返回AssetBundle的直接子级
建议
在多数情况下,最好在玩家进入应用程序的性能关键区域(如主游戏关卡或世界)之前加载尽可能多的所需对象
这在移动平台上尤为重要,因为在移动平台上,访问本地存储的速度很慢,并且在运行时加载和卸载对象会触发垃圾回收
实践
管理已经加载的Asset
AssetBundle.unload(bool)
此API将卸载正在调用的AssetBundle的包头信息。unload参数决定是否也卸载从此AssetBundle实例化的所有对象。如果设置为true,那么从AssetBundle创建的
所有对象也将立即卸载,即使它们目前正在活动场景中被引用
对于大多数项目来说,应该使用AssetBundle.Unload(True)
- 在应用程序的生命周期内定义一个合适的节点,并在此期间卸载不需要的AssetBundle,例如在关卡切换或加载屏幕期间。这是最简单和最常见的选择
- 维护单个对象的引用计数,并仅当所有组成对象都未使用时才卸载AssetBundles。这允许应用程序在不重复内存的情况下卸载和重新加载单个对象
如果必须使用AssetBundle.Unload(False),那么只能通过两种方式卸载各个对象
- 在场景和代码中消除对不需要的对象的所有引用。完成后,调用Resources.UnusedAsset
- 非附加加载场景。这将销毁当前场景中的所有对象并调用Resources.UnusedAsset
发布
客户端发布项目的AssetBundles有两种基本方法:和项目一起安装或安装后再下载它们
移动项目通常选择先安装后下载,以减少初始安装大小,并保持低于相关平台下载的大小限制
和项目一起发布
最简单的方法是放到StreamingAsset文件夹中,此文件夹中包含的任何内容都将复制到最终的应用程序里
运行时可以通过属性Application.StreamingAssets访问本地存储上StreamingAsset文件夹的完整路径。然后通过AssetBundle.LoadFromFile加载
在Android上,StreamingAsset文件夹中的Asset存储在APK中,如果它们被压缩,可能需要更多的时间来加载,因为存储在APK中的文件可以使
用不同的存储算法。并且所使用的算法还可能因Unity版本而异。
安装后下载
交付AssetBundles最简单的方法是将它们放在某个Web服务器上,并通过UnityWebRequest发布。Unity将自动在本地存储上缓存下载的AssetBundles