学习和使用Unity引擎,我们需要对Unity的一些核心概念和基础必须有一定的了解,比如一个Png图片导入到Unity做了哪些事情,哪些文件支持导入,如果我们想要导入一个Unity不存在的资源格式我们要怎么做,比如解析gltf格式并生成prefab这种需求,如何做引用,如何运行时加载等核心基础内容,文章主要介绍一下内容:
有关AssetBundle部分的学习,感兴趣的可以参考AssetStudio这个插件的源码,里面可以大体的看到AssetBundle底层的数据结构是怎么定义的
持续更新和补充完善~
原生对象和托管对象
Native Object:所有继承自UnityEngine.Object的游戏对象在C++引擎底层都会有一个NativeObject对应,记录了完整的Object信息
Managed Objects:C#侧的Wrapper Object
原生引擎对象Native Object和Managed Object有各自的生命周期管理和垃圾回收机制。或者说C#的Object和Native底层的Object生命周期并不是一一对应的。
Unity是一个C++引擎,实际UnityEngine.Object储存数据都在C++侧,所以如果通过C#访问方法或者属性,都要经过一次“引擎”调用,所以尽量减少属性,减少引擎API的调用,减少C#和C++的交互
Unity底层重载了==和!=操作符,C#层UnityEngine.Object(wrapper objects)指向在C++侧的Native Object对象,但是两者的生命周期并不是完全一样的,两者垃圾回收机制也是不一样,比如一个Object已经被Destroy但是C#侧没有触发GC的情况下,如果使用 UnityEngine.Object == Null,Unity会返回true,因为在Native底层Object已经被卸载回收,但是C# Wrapper Object并不是Null
MONO和IL2CPP在处理继承自UnityEngine.Object类的时候有特殊的处理,调用对象实例的成员函数,底层触发调用到底层的引擎代码,需要将C#侧的Object映射到Native层的Object并且检测合法性,检测当前Native Object是否存活,unityObject==null,Unity底层做了一些外的工作,效率要比单纯的比较C#引用类型是否为NULL要低。
如果检测Object对象在Native底层的生命周期,使用== != 或者 ?. ??,如果只是检测某个C#的Object对象是否被赋值,使用object.ReferenceEquals判断是否被赋值。
明确期望生命周期,== != 在Unity做了重载处理,谨慎使用,效率不高
?? ?.是纯C#侧的判断,和Unity底层的生命周期无关
在实际开发中,需要对Object的操作注意以下几点:
数据如何被高效的组织并持久化到文件中,数据如何按照反序列化规则重新被读取和创建,最常见的Json To Object,Object To Json,Message to Binary ,Binary To Message
Unity中的序列化系统使用C++开发,用来做引擎底层native object类型(textures,animationclip,camera,等等)的数据序列化。序列化发生在UnityEngine.Object层级,整个Object和引用都会被完整的序列化
属性必须满足被序列化的条件
可以序列化的数据类型
不支持的数据类型
将Origin原始文件,导入到Unity工程中,生成引擎Native Object Asset的过程(Game-Ready Optimized,Serialized Native Asset)
如何引擎都有自己的资产导入和管理系统
The conversion process is required because most file formats are optimized to save storage space, whereas in a game or a real-time application, the asset data needs to be in a format that is ready for hardware, such as the CPU, graphics, or audio hardware, to use immediately. For example, when Unity imports a .png image file as a texture, it does not use the original .png-formatted data at runtime. Instead, when you import the texture, Unity creates a new representation of the image in a different format which is stored in the Project’s Library folder. The Texture class in the Unity engine uses this imported version, and Unity uploads it to the GPU for real-time display.
Unity资源导入简单分为三个处理步骤
Unity中的Asset可以包含多个SubAssets,比如一个FBX在导入到Unity中可能存在多个Sub-Asset,Material,Animation,Avatar,Model
针对Unity导入原始资产生成引擎Asset的过程,开发者可以Hook到对应的流程
一般通过AssetPostprocessor做资产的检查和ImportSettings的设置,比如常见的设置
Origin Asset 转化为 Unity引擎运行时加载的Native Object Asset格式,AssetDatabase用来管理被转换过后的Native Object Asset
可以通过Window/Analysics/Import Activity查看资源导入情况
Contains the asset’s import settings, and contains a GUID which allows Unity to connect the original asset file with the artifact in the asset database.
fileFormatVersion: 2
guid: dbf51b524b581ec44a55c14e92280e22
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
fileFormatVersion: 2
guid: 85d9545c7c154a743a2a0233004898fa
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:
fileFormatVersion: 2
guid: db164ed4ea6391b40a3c1649b3653d1e
ModelImporter:
serializedVersion: 21300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 0
materialName: 0
materialSearch: 1
materialLocation: 1
animations:
legacyGenerateAnimations: 4
meshes:
userData:
assetBundleName:
assetBundleVariant:
.meta文件中包含三个信息:
由于Meta文件处理不当,可能会造成引用丢失问题:
在分析Unity的资源管理模块的时候,需要区分Asset和Object的区别
几乎所有的Object类型都是内置的,Unity中有两个特殊的类型:
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8002146119907625182}
m_Enabled: 1
m_EditorHideFlags: 0
// 引用一个继承自MonoBehavior的脚本
m_Script: {fileID: 11500000, guid: 96e7330c395831247b6d283dfd987152, type: 3}
m_Name:
m_EditorClassIdentifier:
bConsolePrinting: 1
bSGJDebug: 0
个人理解Unity资源管理中最核心的部分就是“寻址”,需要了解Unity中如何定义引用关系,内部引用,快文件外部引用,底层是如何实现加载一个Object,如何根据一个Instance ID能够从“外部存储空间”中正确的加载Object,为何Unity开发中会出现引用丢失的问题
寻址关系可以简单的理解为,InstanceID <------>FileGUID + LocalID,Unity底层维护这样一个关系映射表,Object是能被正确可寻址的,才能正确的加载ReadObject成功
Unity基于AssetBundle封装的Addressable,翻译成“可寻址”还是很贴切的。
给定某个需要加载的InstanceId,进行寻址(映射关系),寻址成功(映射关系合法),则根据寻址得到的Source源,执行加载,寻址失败,则加载失败。
Unity处理资产组件的引用关系通过FileID + Local ID方式实现,夸文件寻址,(找到文件,定位到文件中的具体位置),通过FileID找到对应的File Asset,通过LocalID定位到具体的Object
File GUID: 一级Asset文件guid,存储在.meta文件中,通过GUID找到Asset文件位置Source源位置,如果meta文件被删除,资源位置并没有发生变化,Unity会重新生成meta文件并保证GUID不会发生变化,保证引用关系是正常的,内部保存了一个GUID和文件路径的映射关系,使用File GUID做文件映射,不用关心Asset File的具体位置,位置发生变更,也不用根据Path更新引用关系
LocalID: 二级文件中的标识ID,一个Asset可能包含多个Sub Asset,定位到具体的Object,每个Asset可以有多个Object组成
AssetDatabase.TryGetGUIDAndLocalFileIdentifier
instanceIDInstanceID:::of the object to retrieve information for.
obj::::The object to retrieve GUID and File Id for.
assetRef::::The asset reference to retrieve GUID and File Id for.
guid:::::The GUID of an asset.
localId::::The local file identifier of this asset.
比较典型的例子:一个Source Asset资源,导入过程的结果是一个或多个UnityEngine.Object。这些在 Unity 编辑器中作为父资源中的多个子资源可见,例如嵌套在作为精灵图集导入的纹理资源下方的多个精灵。这些对象中的每一个都将共享一个File GUID,因为它们的源数据存储在同一资产文件中。它们将在导入的纹理资源中通过Local ID 进行区分和标识。
--- !u!1 &505266372
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 505266376}
- component: {fileID: 505266375}
- component: {fileID: 505266374}
- component: {fileID: 505266373}
m_Layer: 0
m_Name: Plane
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 4294967295
m_IsActive: 1
--- !u!4 &505266376
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 505266372}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 12.396989, y: 5.15, z: 14.302996}
m_LocalScale: {x: 2.3686056, y: 2.5200627, z: 2.6944714}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 921185474}
m_RootOrder: 6
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_Childern 子节点
m_Father 根节点
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8002146119907625182}
m_Enabled: 1
m_EditorHideFlags: 0
// 引用一个继承自MonoBehavior的脚本
m_Script: {fileID: 11500000, guid: 96e7330c395831247b6d283dfd987152, type: 3}
m_Name:
m_EditorClassIdentifier:
bConsolePrinting: 1
bSGJDebug: 0
--- 父节点GameObject
--- !u!1 &921185473
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 921185474}
m_Layer: 0
m_Name: Frame
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &921185474
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 921185473}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1887623492}
- {fileID: 1925342256}
- {fileID: 505266376}
- {fileID: 868233995952783049}
- {fileID: 267251194}
m_Father: {fileID: 1405857324}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
所有的层级关系都会记录到YAML文件中
“— !u!{CLASS ID} &{FILE ID}” which can be analyzed in two parts:(头部信息包含了两个部分)
有些特殊的情况,开发者可以自己直接修改YAML文件,比如修改一个带有动画的节点名称,可能造成动画曲线对修改名称过后的节点无法驱动,因为AnimationCurve底层使用PathName来做的映射,这种情况下,我们可以手动修改Animation File的path来实现Rename节点的操作,保证动画能够正常播放
动画系统通过string-based 路径来识别对应的节点,而不是通过GameObject IDs
Reference to another file object
Unity的脚本也是当作Asset来处理的,MonoBehaviour引用一个真正的Script资源
随着运行时维护的资源增多,通过File GUID和Local ID维护引用关系效率变得低,为了提升索引效率,通过运行时的instanceID快速确定引用关系
Unity引擎会在在底层,运行时Unity会维护一个Instance ID cache(ID to Object*指针),File GUID + Local ID确定唯一的一个InstanceID,在运行时更加的高效做引用,Unity在序列化的时候,通过PPtr表示引用了一个Object对象,通过PPtr的Transfer构建寻址关系(instanceId<->FileID + LocalFileID)
在Unity RunTime启动时,会首先初始化InstanceId Cache,将打包Scene所引用的资源和Resources目录下的所有资源,根据File GUID + Local ID生成对应的Instance ID添加到Cache中(这个也是不建议在Resouces中存放较多资源的原因,资源越多,生成Instance ID数量越多速度会越慢),Resources中的文件会被序列化成一个data文件,在APP启动的时候需要根据Resources中的资源名字建立索引查找树,通过AssetName能够获取到对应的File GUID和Local ID,从而进行加载资源,Indexing lookup tree来用来确定指定的Object在在序列化文件中的读取位置读取大小等信息
运行时动态Load进来的资源,比如通过Assetbundle加载的Asset,也会加载到Unity运行时维护的Instance ID Cache中(BaseObject维护的IDToPointerMap映射表),只有在AssetBundle被UnLoad的时候,涉及到加载到内存中的Asset的Instance ID才会被从cache移除,节省内存。下次重新加载AssetBundle,加载相应的Asset,Unity会重新创建新的Intance ID。
游戏对象层级Hierarchy会被完整的的序列化,所有的GameObject和Component都会被序列化到数据中,所以如果Hierarchy层级越复杂,序列化数据会越大,导致Unity在加载并建立游戏对象的Hierarchy有一定的性能开销。
在创建GameObject游戏对象Hierarchy的时候,Unity需要做的几项工作:
一个UI Prefab,包含了30个相同的元素,那么序列化数据将会包含30遍相同元素的序列化数据。生成大量的二进制序列化数据。在加载的时候也是一样会读取和反序列化30遍相同的元素。这样会比较耗费时间。
Unity提供了多种运行时资源管理方案,一种是Resources文件夹模式,一种是AssetBundle,接下来一一介绍这两种方案
将资源文件放到Resources文件夹中,运行时可以通过Resources API动态的加载和卸载其中的Asest资源。
Unity引擎在打包游戏的时候会将整个Resources文件夹序列到一个文件中(serialized file),对应的在PersistentManager中维护的一个SerializedFile文件,该文件包含了索引信息indexing information和metadata原数据,索引信息包含了从资源名称到File GUID和LocalID的映射,同时还有一个AssetInfor列表存储,每个游戏对象Object在二进制文件中的字节位置和读取大小,定位到Object加载的字节地址位置和内容。通常平台,查找关系使用平衡二叉搜索树来构建表示,构建的时间复杂度是nlogn,所以如果Resources文件越多,indexing information构建的耗时也会比较的长。indexing information索引信息的构建在APP或者游戏启动的时候(unity spalsh screen出现)必须要做的工作且无法跳过,例如游戏的首贞画面并没有使用到Resources中的文件,但是从APP启动到进入到首场景也会经历Resources文件夹初始化,indexing information构建的过程,会造成APP或者游戏启动速度变慢。
官方的实际测试数据,一个拥有10000个Assets的Resources目录,在低端移动设备上的初始化需要5-10秒甚至更长。但其实,这些Assets并不会在一开始就全部用到。
继续介绍下,Unity提供的另外一种资源加载方案Assetbundle
Assetbundle本质上是一个包含了各种Game Ready的资源存档文件或者是一个容器(models,贴图,prefab,audio clip,场景等等),本质上就是一个二进制数据存档文件Archive File,在PersistentManager中维护加载的AssetBundle容器)参考下文PersistentManager中的讲解
官方介绍,AssetBundle文件实际上包含两部分的内容:
Header + Data Segment
Assetbundle头部信息,包含标识符identifier,压缩类型compression type,manifest内容清单,manifest是一个以Object资源名称为Key的查找字典,每个字典元素确定了当前Object在data segment中的字节索引位置。用来加载Object。在大多数的平台中,查找字典底层是一个平衡二叉搜索树,Windows和OSX-derived 平台(IOS)使用的是红黑树,查找字典的运行时构建和当前Assetbundle的资源量成正比,资源越多,查找字典构建越耗时。
Data Segment是Assetbundle数据部分,包含了真正的Object对象实际的数据,Unity提供了三种AssetBundle压缩方式:
当加载一个AsesetBundle的时候,Unity会开辟一定的内存空间用来存储AssetBundle数据,加载到内存中通过Memory Profiler查看SerializedFile占用的内存,Unity底层是通过PersistentManager来管理和维护Serialized File对象
CAB-开头的可以理解为从Assetbundle加载的Serialized File
AssetBundle有以下几点内存占用:
UnityEngine.Object可以通过Unity提供的API从Assetbundle中加载,Unity提供了同步和异步加载方法
UnityEngine.Object的加载逻辑,从存储读取Object数据在Work Thread工作线程中执行,所有不涉及处理Unity线程敏感部分,比如脚本,图像相关的操作都会在Work Thread工作线程中执行,比如VBO创建,纹理解压缩。
异步加载方法Asynchronous Load(Resources.LoadAsync,Assetbundle.LoadAssetAsync,Assetbundle.LoadAllAssetAsync),scenes(SceneManager.LoadSceneAsync)将会在单独的Work thread执行Object对象的读取,反序列化非线程敏感的操作,然后在主线程MainThread执行Object交互(object integration),会调用Object.AwakeFromLoad方法,具体执行的逻辑和Object对象的类型有关,针对textures在调用AwakeFromLoad的时候会执行UploadTexture,meshes(UploadMeshData),向GPU上传数据,如果是audio数据,交互代表在主线程准备音频数据并准备播放。
加载 UnityEngine.Object = Work Thread Part(loading,deserialization,and so on) + Main thread Part(Object integration)AwakeOnLoad
都完成,比如Gameobject,Object对象会调用Awake,表示当前Object已经加载完毕,可以使用
在主线程的Object交互操作为了不阻塞主线程造成卡顿,Unity提供了Application.backgroundLoadingPriority设置异步方法可以在单帧中执行的最长时间,如果为了加快读取时间,可以使用High等级,加快Object加载速度。具体的应该设置为多少,需要根据项目和实际的加载场景设置。
这部分属于老生常谈的知识点了,我们在做一个介绍,AssetBundle.Unload,将加载的Header Information删除,提供的unloadAllLoadedObjects,该参数规定了,AssetBundle被Unload之后对已经加载从AssetBundle中加载的Object如何做处理,而且我们是没有办法将AssetBundle的一部分资源给卸载掉的,只能完整的卸载整个AssetBundle
但是并不意味着我们项目中一定都要用UnLoad(True)
不同类型的项目,打包分包规则可能差别会很大
在使用Unity引擎开发的过程中我们必须对Unity的资源内存有一定的认知,这样才能尽可能的减少开发过程中带来的内存问题
为了更好管理运行时内存,需要了解Asset的生命周期管理,资源卸载和加载的时机。
寻址Asset Source源,找得到Object的源地址(File GUID + Local ID)Source location,就可以加载Object,找不到Source加载就会失败
AssetBundle.UnLoad将Header information销毁,加载AssetBundle中的Asset就会寻址失败
可以通过API手动加载Object,比如AssetBundle.LoadAsset等等
一个Object游戏对象被自动加载到内存:
当一个Object被加载的时候,Unity会检测当前加载的Object所引用包含的所有File ID+LocalID,生成对应的InstanceID,同时执行PPtr解引用操作。
(File ID+Local ID和Instance ID的映射不合法意味着,Object无法寻址对应的加载源Source(Serialized File),无法加载到内存中,寻址不到Source源。)
如果调用了Unload(false), 映射关系会被清空但是从AssetBundle中加载的Object不会被销毁,后续运行中,Unity如果触发了必须从Assetbundle中Reload资源的情况,会导致加载失败。一种可能会出现的问题的场景如下:
已经不会被使用的资源仍然在内存中,一直被引用,导致GC的时候,无法被卸载,比如Lua脚本中如果引用到了C#中的Object,如果没有被正确的处理,很容易造成内存泄露
public override bool Equals(object o) { return CompareBaseObjects (this, o as Object); }
CSRAW private static bool CompareBaseObjects (Object lhs, Object rhs)
{
#if UNITY_WINRT
return UnityEngineInternal.ScriptingUtils.CompareBaseObjects(lhs, rhs);
#else
return CompareBaseObjectsInternal (lhs, rhs);
#endif
}
// Compares if two objects refer to the same
CSRAW public static bool operator == (Object x, Object y) { return CompareBaseObjects (x, y); }
// Compares if two objects refer to a different object
CSRAW public static bool operator != (Object x, Object y) { return !CompareBaseObjects (x, y); }
CONSTRUCTOR_SAFE
CUSTOM private static bool CompareBaseObjectsInternal ([Writable]Object lhs, [Writable]Object rhs)
{
return Scripting::CompareBaseObjects (lhs.GetScriptingObject(), rhs.GetScriptingObject());
}
/// Compares two Object classes.
/// Returns true if both have the same instance id
/// or both are NULL (Null can either mean that the object is gone or that the instanceID is 0)
bool CompareBaseObjects (ScriptingObjectPtr lhs, ScriptingObjectPtr rhs)
{
int lhsInstanceID = 0;
int rhsInstanceID = 0;
bool isLhsNull = true, isRhsNull = true;
if (lhs)
{
lhsInstanceID = GetInstanceIDFromScriptingWrapper (lhs);
ScriptingObjectOfType<Object> lhsRef(lhs);
isLhsNull = !lhsRef.IsValidObjectReference();
}
if (rhs)
{
rhsInstanceID = GetInstanceIDFromScriptingWrapper (rhs);
ScriptingObjectOfType<Object> rhsRef(rhs);
isRhsNull = !rhsRef.IsValidObjectReference();
}
if (isLhsNull || isRhsNull)
return isLhsNull == isRhsNull;
else
return lhsInstanceID == rhsInstanceID;
}
所有外部加载的Source File(Serialized File)维护,所有Object的读取都必须通过PersistentManager加载,维护寻址关系表Remapper(InstanceID <-> FileGUID + LocalID)
一个Object可寻址,表示从SerializedFile中能找到对应的Source源,我们可以使用Profiler中查看当前加载的所有Serialized File信息
operator T* () const;
T* operator -> () const;
T& operator * () const;
// Finds the pointer to the object referenced by instanceID (NULL if none found in memory)
static Object* IDToPointer (int inInstanceID);
template<class T> inline
PPtr<T>::operator T* () const
{
if (GetInstanceID () == 0)
return NULL;
Object* temp = Object::IDToPointer (GetInstanceID ());
if (temp == NULL)
temp = ReadObjectFromPersistentManager (GetInstanceID ());
return static_cast<T*> (temp);
}
Object* PersistentManager::ReadObject (int heapID)
{
Object* o = LoadFromActivationQueue(heapID);
if (o != NULL)
{
m_Mutex.Unlock();
return o;
}
// 根据InstanceId进行寻址拿到合法的持久化Stream
// 找到instanceId 和 FileID + LocalId的映射
// File ID 确定了Stream
// Find and load the right stream
SerializedObjectIdentifier identifier;
if (!m_Remapper->InstanceIDToSerializedObjectIdentifier(heapID, identifier))
{
m_Mutex.Unlock();
return NULL;
}
// 根据已经被转换的streamIndex,拿到PersistentManager保存的stream进行读取
SerializedFile* stream = GetSerializedFileInternal (identifier.serializedFileIndex);
if (stream == NULL)
{
#if DEBUG_MAINTHREAD_LOADING
LogString(Format("--- Loading from main thread failed loading stream %f", (GetTimeSinceStartup () - time) * 1000.0F));
#endif
m_Mutex.Unlock();
return NULL;
}
// Find file id in stream and read the object
// 根据fileid(local id in a file)读取对应的Object
m_ActiveNameSpace.push (identifier.serializedFileIndex);
TypeTree* oldType;
bool didTypeTreeChange;
o = NULL;
stream->ReadObject (identifier.localIdentifierInFile, heapID, kCreateObjectDefault, true, &oldType, &didTypeTreeChange, &o);
m_ActiveNameSpace.pop ();
// Awake the object
if (o)
{
AwakeFromLoadQueue::PersistentManagerAwakeSingleObject (*o, oldType, kDidLoadFromDisk, didTypeTreeChange, gSafeBinaryReadCallback);
}
return o;
}
// Reads the object referenced by id from disk
// Returns a pointer to the object. (NULL if no object was found on disk)
// object is either PRODUCED or the object already in memory referenced by id is used
// isMarkedDestroyed is a returned by value (non-NULL)
// registerInstanceID should the instanceID be register with the ID To Object lookup (false for threaded loading)
// And reports whether the object read was marked as destroyed or not
void ReadObject (LocalIdentifierInFileType fileID, int instanceId, ObjectCreationMode mode, bool isPersistent, TypeTree** oldTypeTree, bool* didChangeTypeTree, Object** readObject);
void SerializedFile::ReadObject (LocalIdentifierInFileType fileID, int instanceId, ObjectCreationMode mode, bool isPersistent, TypeTree** oldTypeTree, bool* didChangeTypeTree, Object** outObjectPtr)
{
// 检测当前localFileID是否在该SerializedFile中存在
// typedef vector_map ObjectMap;
// 读取的ObjectInfo列表,表示当前File所包含的所有Objects信息
ObjectMap::iterator iter = m_Object.find (fileID);
// 获取ObjectInfo执行创建
const ObjectInfo& info = iter->second;
// Create empty object
Object* objectPtr = *outObjectPtr;
if (objectPtr == NULL)
{
*outObjectPtr = objectPtr = Object::Produce (info.classID, instanceId, kMemBaseObject, mode);
}
// Type Tree?
// 选择合适的Transfer底层序列化工具执行反序列化操作
else
{
#if SUPPORT_SERIALIZED_TYPETREES
StreamedBinaryRead<true> readStream;
CachedReader& cache = readStream.Init (options);
cache.InitRead (*m_ReadFile, byteStart, info.byteSize);
Assert(m_ResourceImageGroup.resourceImages[0] == NULL);
// Read the object
objectPtr->VirtualRedirectTransfer (readStream);
int position = cache.End ();
if (position - byteStart != info.byteSize)
OutOfBoundsReadingError (info.classID, info.byteSize, position - byteStart);
*didChangeTypeTree = false;
#else
AssertString("reading endian swapped is not supported");
#endif
}
}
// Awake the object
if (o)
{
AwakeFromLoadQueue::PersistentManagerAwakeSingleObject (*o, oldType, kDidLoadFromDisk, didTypeTreeChange, gSafeBinaryReadCallback);
}
o.AwakeFromLoad (awakeMode);
o.ClearPersistentDirty ();
InstanceID和Object映射关系,InstanceId<->ObjectPtr,PPtr是一个指向Object对象的指针。需要的时候延迟加载Object对象(解引用),PPtr的Transfer可以理解为寻址注册,映射关系构建,Unity通过InstanceID来记录引用关系,PPtr指向一个Object对象,在执行实例化Instantiate的时候也是需要将PPtr指向的Object进行拷贝操作
AssetBundle中表示当前Bundle中所有的Asset指针定义了FileID+LocalFileID,在AssetBundle进行Transfer的时候,遇到PPtr会执行PPtr的Transfer
map m_Container
Array Array
int size = 31
[0]
pair data
string first = "assets/res/character/npc/shop_batai_001.prefab"
AssetInfo second
int preloadIndex = 569
int preloadSize = 26
PPtr<Object> asset
int m_FileID = 0
SInt64 m_PathID = 1810623846293925203
[1]
pair data
string first = "assets/res/character/npc/shop_batai_002.prefab"
AssetInfo second
int preloadIndex = 434
int preloadSize = 31
PPtr<Object> asset
int m_FileID = 0
SInt64 m_PathID = -287100744004624113
[2]
pair data
string first = "assets/res/character/npc/shop_caidan_001.prefab"
AssetInfo second
int preloadIndex = 150
int preloadSize = 27
PPtr<Object> asset
int m_FileID = 0
SInt64 m_PathID = -2163428893405175807
[3]
pair data
string first = "assets/res/character/npc/shop_deng_001.prefab"
AssetInfo second
int preloadIndex = 512
int preloadSize = 22
PPtr<Object> asset
int m_FileID = 0
SInt64 m_PathID = 495874309500947719
template<class T>
class PPtr
{
SInt32 m_InstanceID;
#if !UNITY_RELEASE
mutable T* m_DEBUGPtr;
#endif
protected:
inline void AssignObject (const Object* o);
private:
static string s_TypeString;
public:
static const char* GetTypeString ();
static bool IsAnimationChannel () { return false; }
static bool MightContainPPtr () { return true; }
static bool AllowTransferOptimization () { return false; }
template<class TransferFunction>
void Transfer (TransferFunction& transfer);
// Assignment
explicit PPtr (int instanceID)
{
m_InstanceID = instanceID;
#if !UNITY_RELEASE
m_DEBUGPtr = NULL;
#endif
}
PPtr (const T* o) { AssignObject (o); }
PPtr (const PPtr<T>& o)
{
m_InstanceID = o.m_InstanceID;
#if !UNITY_RELEASE
m_DEBUGPtr = NULL;
#endif
}
PPtr ()
{
#if !UNITY_RELEASE
m_DEBUGPtr = NULL;
#endif
m_InstanceID = 0;
}
PPtr& operator = (const T* o) { AssignObject (o); return *this; }
PPtr& operator = (const PPtr<T>& o)
{
#if !UNITY_RELEASE
m_DEBUGPtr = NULL;
#endif
m_InstanceID = o.m_InstanceID; return *this;
}
void SetInstanceID (int instanceID) { m_InstanceID = instanceID; }
int GetInstanceID ()const { return m_InstanceID; }
// Comparison
bool operator < (const PPtr& p)const { return m_InstanceID < p.m_InstanceID; }
bool operator == (const PPtr& p)const { return m_InstanceID == p.m_InstanceID; }
bool operator != (const PPtr& p)const { return m_InstanceID != p.m_InstanceID; }
operator T* () const;
T* operator -> () const;
T& operator * () const;
};
PPtr解引用操作
Object被从外部Load到内存并加载的时机:当前PPtr被“解引用”的时候,会触发从PersistentManager中读取Object,通常情况下只会持有InstanceID,只有真正的被“解引用”访问的时候,才会从持久化管理器中ReadObject加载对应的Object
template<class T> inline
T& PPtr<T>::operator * () const
{
// 如果当前Object在运行时IDMap中不存在
// 从PersistentManager读取Object
Object* temp = Object::IDToPointer (GetInstanceID ());
if (temp == NULL)
temp = ReadObjectFromPersistentManager (GetInstanceID ());
#if !UNITY_RELEASE
m_DEBUGPtr = (T*) (temp);
#endif
#if DEBUGMODE || !GAMERELEASE
T* casted = dynamic_pptr_cast<T*> (temp);
if (casted != NULL)
return *casted;
else
{
if (temp != NULL)
{
ErrorStringObject ("PPtr cast failed when dereferencing! Casting from " + temp->GetClassName () + " to " + T::GetClassStringStatic () + "!", temp);
}
else
{
ErrorString ("Dereferencing NULL PPtr!");
}
ANALYSIS_ASSUME(casted);
return *casted;
}
#else
return *static_cast<T*> (temp);
#endif
}
//
// 从PersistentManager中根据InstanceID读取加载UnityEngine.Object,解引用PPtr
//
Object* ReadObjectFromPersistentManager (int id)
{
if (id == 0)
return NULL;
else
{
// In the Player it is not possible to call MakeObjectPersistent,
// thus instance id's that are positive are the only ones that can be loaded from disk
#if !UNITY_EDITOR
if (id < 0)
{
#if DEBUGMODE
//AssertIf(GetPersistentManager ().ReadObject (id));
#endif
return NULL;
}
#endif
Object* o = GetPersistentManager ().ReadObject (id);
return o;
}
}
PPtr解引用的时机
m_PPtrCurves,引用外部的Object对象,PPtr Curve类型EditorCurveBinding.PPtrCurve,指向Object的动画曲线,比如给一个Image在动画文件中做一个序列帧动画,每一帧都会引用一个外部的Sprite对象
m_EulerCurves: []
m_PositionCurves: []
m_ScaleCurves: []
m_FloatCurves: []
m_PPtrCurves:
- curve:
- time: 0
value: {fileID: 0}
- time: 0.033333335
value: {fileID: 21300000, guid: ff2bba53a71aed5468e208397796e1c9, type: 3}
- time: 0.05
value: {fileID: 21300000, guid: 8c9de47e31a111d4996d705840dba765, type: 3}
- time: 0.06666667
value: {fileID: 21300000, guid: 3127dd5fdad8c1148850546ce86c831b, type: 3}
- time: 0.083333336
value: {fileID: 21300000, guid: 1dc8c048bb3b7be408250bc0263d4169, type: 3}
- time: 0.1
value: {fileID: 21300000, guid: acb5c2a66fd2211459bf7ec94ab220d2, type: 3}
- time: 0.11666667
value: {fileID: 21300000, guid: 062d0d1b5a5462046a980205b54b8626, type: 3}
- time: 0.31666666
value: {fileID: 0}
attribute: m_Sprite
path:
classID: 114
// Monobehaviour
// Image.cs
script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!74 &7400000
AnimationClip:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: New Animation
serializedVersion: 6
m_Legacy: 0
m_Compressed: 0
m_UseHighQualityCurve: 1
m_RotationCurves: []
m_CompressedRotationCurves: []
m_EulerCurves: []
m_PositionCurves: []
m_ScaleCurves: []
m_FloatCurves: []
m_PPtrCurves:
- curve:
- time: 0
value: {fileID: 0}
- time: 0.033333335
value: {fileID: 21300000, guid: ff2bba53a71aed5468e208397796e1c9, type: 3}
- time: 0.05
value: {fileID: 21300000, guid: 8c9de47e31a111d4996d705840dba765, type: 3}
- time: 0.06666667
value: {fileID: 21300000, guid: 3127dd5fdad8c1148850546ce86c831b, type: 3}
- time: 0.083333336
value: {fileID: 21300000, guid: 1dc8c048bb3b7be408250bc0263d4169, type: 3}
- time: 0.1
value: {fileID: 21300000, guid: acb5c2a66fd2211459bf7ec94ab220d2, type: 3}
- time: 0.11666667
value: {fileID: 21300000, guid: 062d0d1b5a5462046a980205b54b8626, type: 3}
- time: 0.31666666
value: {fileID: 0}
attribute: m_Sprite
path:
classID: 114
// Monobehaviour 114
// Image.cs fe87c0e1cc204ed48ad3b37840f39efc
script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_SampleRate: 60
m_WrapMode: 0
m_Bounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 0, y: 0, z: 0}
m_ClipBindingConstant:
genericBindings:
- serializedVersion: 2
path: 0
attribute: 2015549526
script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
typeID: 114
customType: 0
isPPtrCurve: 1
pptrCurveMapping:
- {fileID: 0}
- {fileID: 21300000, guid: ff2bba53a71aed5468e208397796e1c9, type: 3}
- {fileID: 21300000, guid: 8c9de47e31a111d4996d705840dba765, type: 3}
- {fileID: 21300000, guid: 3127dd5fdad8c1148850546ce86c831b, type: 3}
- {fileID: 21300000, guid: 1dc8c048bb3b7be408250bc0263d4169, type: 3}
- {fileID: 21300000, guid: acb5c2a66fd2211459bf7ec94ab220d2, type: 3}
- {fileID: 21300000, guid: 062d0d1b5a5462046a980205b54b8626, type: 3}
- {fileID: 0}
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {fileID: 0}
m_AdditiveReferencePoseTime: 0
m_StartTime: 0
m_StopTime: 0.33333334
m_OrientationOffsetY: 0
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 1
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
m_LoopBlendPositionXZ: 0
m_KeepOriginalOrientation: 0
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 0
m_EditorCurves: []
m_EulerEditorCurves: []
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []
定义一个Class,成员的内存布局,A TypeTree describes the field layout of one of your data types,Per type not per object,数据安全和版本 data safety and versioning,不能跨AssetBundle共享,每个AssetBundle都有TypeTree信息
Transfer的过程中,TypeTree用来定义Object,成员变量的读取规则,给一段字节流,根据TypeTree我们可以解析出来每个字段的类型和具体内容,或者简单的理解为“解码规则”,如果去读取一个不同版本引擎打包的AssetBundle内容,不同的引擎版本或者我们自己定义的脚本,同一个Class可能有字段差异,所以如果直接按照当前Class进行解析,会导致解析报错。
AssetBundle中有TypeTree信息,可以进行比对Class哪些字段的类型变更或者字段增加删除,新增的给默认值,删除字段也给默认值,做到保证读取数据安全和做到版本兼容
TypeTree不能共享,每个AssetBundle必须包含自己的TypeTree数据,这样就会造成信息冗余,内存有额外的开销,而且TypeTree是根据Class type定义的,而不是根据每个Object来定义
不少大厂都针对TypeTree做了引擎级别的定制优化,如果没有版本兼容问题,TypeTree可以通过BuildAssetBundleOptions.DisableWriteTypeTree,在打包AssetBundle的时候不生成TypeTree信息,这样ab包会更小加载速度更快(直接读取并反序列化Transfer不用创建TypeTree,检测是否合法并做修正),但是无法做到版本兼容性,加载不同版本的Assetbundle会加载失败,针对特定运行平台必须强制携带类型信息,typetree是无法剔除的,比如WebGL平台,Assetbundle中必须携带TypeTree信息
<assembly fullname="UnityEngine">
<type fullname="UnityEngine.AI.NavMeshObstacle" preserve="all"/>
<type fullname="UnityEngine.AI.NavMeshData" preserve="all"/>
<type fullname="UnityEngine.AI.NavMeshAgent" preserve="all"/>
<type fullname="UnityEngine.AI.NavMeshPath" preserve="all"/>
<type fullname="UnityEngine.Collider" preserve="all"/>
<type fullname="UnityEngine.MeshCollider" preserve="all"/>
<type fullname="UnityEngine.SkinnedMeshRenderer" preserve="all"/>
<type fullname="UnityEngine.Avatar" preserve="all"/>
<type fullname="UnityEngine.LODGroup" preserve="all"/>
<type fullname="UnityEngine.LightProbeGroup" preserve="all"/>
<type fullname="UnityEngine.Animations.PositionConstraint" preserve="all"/>
</assembly>
可以概括的在调用Object.Instantiate的时候,Unity分三个步骤Produce,Copy,Awake,如果在Profiler中开启Deep Profile模式,可以看到在实例化的时候,Unity底层具体做了哪些事情,可以简单的概括为以下几个详细步骤:
Object& InstantiateObject (Object& inObject, const Vector3f& worldPos, const Quaternionf& worldRot)
{
TempRemapTable ptrs;
Object& obj = InstantiateObject (inObject, worldPos, worldRot, ptrs);
AwakeAndActivateClonedObjects(ptrs);
return obj;
}
static Object* CloneObjectImpl (Object* object, TempRemapTable& ptrs)
{
// 采集Clone目标Object的所有的子节点和组件信息,执行Object.Produce创建操作
CollectAndProduceClonedIsland (*object, &ptrs);
TempRemapTable::iterator it;
#if UNITY_FLASH
//specialcase for flash, as that needs to be able to assume linear memorylayout.
dynamic_array<UInt8> buffer(kMemTempAlloc);
MemoryCacheWriter cacheWriter (buffer);
#else
BlockMemoryCacheWriter cacheWriter (kMemTempAlloc);
#endif
// 执行Clone数据操作
// Origin Key:源PPtr
// Clone Value:将数据反序列到Cloned PPtr
RemapFunctorTempRemapTable functor (ptrs);
RemapPPtrTransfer remapTransfer (kSerializeForPrefabSystem, true);
remapTransfer.SetGenerateIDFunctor (&functor);
for (it=ptrs.begin ();it != ptrs.end ();it++)
{
Object& original = *PPtr<Object> (it->first);
// Copy Data
Object& clone = *PPtr<Object> (it->second);
StreamedBinaryWrite<false> writeStream;
CachedWriter& writeCache = writeStream.Init (kSerializeForPrefabSystem, BuildTargetSelection::NoTarget());
writeCache.InitWrite (cacheWriter);
original.VirtualRedirectTransfer (writeStream);
writeCache.CompleteWriting();
StreamedBinaryRead<false> readStream;
CachedReader& readCache = readStream.Init (kSerializeForPrefabSystem);
readCache.InitRead (cacheReader, 0, writeCache.GetPosition());
clone.VirtualRedirectTransfer (readStream);
readCache.End();
if (!IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_0_a1))
{
GameObject* clonedGameObject = dynamic_pptr_cast<GameObject*> (&clone);
if (clonedGameObject)
clonedGameObject->SetActiveBitInternal(true);
}
// Remap references
clone.VirtualRedirectTransfer (remapTransfer);
}
//
TempRemapTable::iterator found = ptrs.find (object->GetInstanceID ());
AssertIf (found == ptrs.end ());
object = PPtr<Object> (found->second);
return object;
}
void CollectAndProduceClonedIsland (Object& o, TempRemapTable* remappedPtrs)
{
AssertIf(!remappedPtrs->empty());
remappedPtrs->reserve(64);
GameObject* go = GetGameObjectPtr(o);
if (go)
{
///@TODO: It would be useful to lock object creation around a long instantiate call.
// Butwe have to be careful that we dont load anything during the object creation in order to avoid
// a deadlock: case 389317
CollectAndProduceGameObjectHierarchy(*go, go->QueryComponent(Transform), remappedPtrs);
}
else
CollectAndProduceSingleObject(o, remappedPtrs);
remappedPtrs->sort();
}
Transform* CollectAndProduceGameObjectHierarchy (GameObject& go, Transform* transform, TempRemapTable* remappedPtrs)
{
GameObject* cloneGO = static_cast<GameObject*> (Object::Produce (ClassID(GameObject)));
remappedPtrs->insert(make_pair(go.GetInstanceID(), cloneGO->GetInstanceID()));
GameObject::Container& goContainer = go.GetComponentContainerInternal();
GameObject::Container& clonedContainer = cloneGO->GetComponentContainerInternal();
clonedContainer.resize(goContainer.size());
for (int i=0;i<goContainer.size();i++)
{
Unity::Component& component = *goContainer[i].second;
Unity::Component& clone = static_cast<Unity::Component&> (ProduceClone(component));
clonedContainer[i].first = goContainer[i].first;
clonedContainer[i].second = &clone;
clone.SetGameObjectInternal(cloneGO);
remappedPtrs->insert(make_pair(component.GetInstanceID(), clone.GetInstanceID()));
}
if (transform)
{
Transform& cloneTransform = cloneGO->GetComponent(Transform);
Transform::TransformComList& srcTransformArray = transform->GetChildrenInternal();
Transform::TransformComList& dstTransformArray = cloneTransform.GetChildrenInternal();
dstTransformArray.resize_uninitialized(srcTransformArray.size(), false);
for (int i=0;i<srcTransformArray.size();i++)
{
Transform& curT = *srcTransformArray[i];
GameObject& curGO = curT.GetGameObject();
Transform* curCloneTransform = CollectAndProduceGameObjectHierarchy(curGO, &curT, remappedPtrs);
curCloneTransform->GetParentPtrInternal() = &cloneTransform;
dstTransformArray[i] = curCloneTransform;
}
return &cloneTransform;
}
else
{
return NULL;
}
}
https://github.com/Unity-Technologies/UnityDataTools/blob/main/UnityDataTool/README.md
从多个维度去查看AssetBundle的信息
./UnityDataTool analyze ./ -o my_k.db -p *.bundle
将bundle信息输出到sqlite数据库中
通过https://sqlitebrowser.org/ 工具查看
https://github.com/Perfare/AssetStudio