【Unity引擎核心-Object,序列化,资产管理,内存管理】

文章目录

  • 整体介绍
  • Native & Managed Objects
  • 什么是序列化
    • 序列化用来做什么
    • Editor和运行时序列化的区别
    • 脚本序列化
    • 针对序列化的使用建议
  • Unity资产管理
    • 导入Asset Process
    • 为何要做引擎资源文件导入
    • Main-Assets和 Sub-Assets
    • 资产的导入管线Hook,AssetPostprocessor
    • The Asset DataBase
    • Metadata资源元数据
    • Asset和实例对象UnityEngine.Objects的区别
    • Unity中的资源寻址
    • File GUID+ LocalID
    • YAML解析
      • 跨文件索引
      • 依赖引用关系可以编写的工具(不限于)
    • Instance ID
    • Loading large hierarchies加载复杂层级对象
      • 典型案例和优化
  • Resources资源文件夹(运行时加载)
    • 不建议
    • 建议
    • 序列化
  • AssetBundle基础(运行时加载)
    • 什么是Assetbundle
    • 优势
    • 数据结构
      • Header
      • Data Segment
    • 内存占用分析
    • AssetBundle的加载
    • AB包中的Assets加载
    • Asset加载底层实现
    • AssetBundle内存管理
      • Unload(false)的问题
    • AssetBundle使用中需要注意的问题
    • 常见的Assetbundle生命周期管理方案
    • Assetbundle分包策略
    • Assetbundle更新方案
  • Unity资源内存管理
    • 资源生命周期Resoruce lifecycle
      • 加载时机
      • 卸载时机
      • 内存泄露
  • 需要理解的核心基础知识
    • UnityEngine.Object
      • Object的 == 和 != 的重载
    • PersistentManager,Unity持久化寻址管理器
      • Profiler中对应的持久化数据源类型(Serialized File)
      • !!!Object加载流程底层实现
    • PPtr<T>
      • PPtr在序列化文件中的数据结构
      • PPtr<T>类定义
      • PPtr Curve,引用Object的曲线
    • TypeTree
    • ClassID
    • 在调用Object.Instantiate,Unity做了哪些事情
  • 参考资料

整体介绍

学习和使用Unity引擎,我们需要对Unity的一些核心概念和基础必须有一定的了解,比如一个Png图片导入到Unity做了哪些事情,哪些文件支持导入,如果我们想要导入一个Unity不存在的资源格式我们要怎么做,比如解析gltf格式并生成prefab这种需求,如何做引用,如何运行时加载等核心基础内容,文章主要介绍一下内容:

  • Unity中的序列化
  • Unity资产导入流程和详细实现
  • Unity如何做文件内索引和夸文件索引
  • 介绍Unity提供的两种资源动态加载方案(Resources和AssetBundle)
  • 介绍AssetBundle的数据格式和使用方法
  • 配合第三方的工具分析AssetBundle的格式
  • PPtr,PersistentManager,TypeTree等核心概念

有关AssetBundle部分的学习,感兴趣的可以参考AssetStudio这个插件的源码,里面可以大体的看到AssetBundle底层的数据结构是怎么定义的

持续更新和补充完善~

Native & Managed Objects

原生对象和托管对象
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的操作注意以下几点:

  • 明确自己的意图,比如是要做引用是否被赋值还是要判断底层NativeObject是否为NULL
  • 减少Object的使用,减少对属性的访问,减少C++和C#之间的交互
  • 对Object是一个很重的操作,尽量避免在Update中使用UnityEngne.Object的操作和访问,C++,C#调用,类型判断,合法性检测等等,Object更像是一个被持有的表现层,必须要用到的时候才去用,在Update中尽量减少Object == null或者Object != null的判断Unity中的序列化

什么是序列化

数据如何被高效的组织并持久化到文件中,数据如何按照反序列化规则重新被读取和创建,最常见的Json To Object,Object To Json,Message to Binary ,Binary To Message

序列化用来做什么

  1. Saving/Loading data to/from memory or disk
    • 文件格式,binary二进制文件,YAML,json等等
    • undo,animation,editors
  2. Non-serialisation cases
    • Instantiation
    • Unloading unused assets
    • Remapping references in prefabs
    • Generating type metadata(“typetree”)
  3. 脚本中的属性变量保存 storing data stored in scripts
  4. Editor Inspector Windows属性面板,通过序列化文件serializedobject去保存和展示serializedobject
    • SerializedObject SerializedProperty
    • 直接的序列化对象
    • 使用TypeTree遍历成员
    • 不许用C#侧的Wrapper Class
  5. Prefabs预设,prefab本身就是一个序列化的数据包含了多个gameobjects或者components组件,prefab其实只存在在editor层面,在构建对的时候,所有对prefab做的变动都会被应用到正常的序列化过程中,实例化的时候,本身并不关系是不是prefab,Unity Editor实例化一个游戏对象GameObject根据两个数据(the Prefab source and the Prefab instance’s modifications.)
  6. Instantiation实例化,实例化一个prefab,gameobject所有继承自UnityEngine.Object都可以被序列化,当开发者调用Instantiate(object)的时候,先对object执行序列化操作,创建一个新的object,将实例化对象object的数据反序列化到新的object,如果引用“外部”资源比如纹理贴图,保持引用,如果引用“内部”比如引用子节点,会直接重定向到新创建对象的子节点
  7. Saving保存,如果使用文本编辑器打开一个.unity文件,“force text serialization”打开,我们将会使用采用yaml序列化接口重新序列化对象
  8. Loading,unity底层的加载系统也是建立在序列化基础之上,eidtor模式下的yaml文件读取,运行时加载场景和asset加载,Assetbundles的加载也是使用序列化系统
  9. Hot reloading of Editor code,当修改脚本,Unity会将所有的windows做一次序列化操作,删除老的windows重新加载新的C#代码,重新创建windows,然后将序列化的数据重新反序列化到windows
  10. Resources.GarbageCollectSharedAssets(),unity底层的垃圾回收器,不同于C#的垃圾回收器,unity底层的垃圾回收器是用来确定那些游戏对象未被引用,(比如加载场景过后no-additive,或者手动触发了Resources.UnloadUnuedAssets),将遍历出来的游戏对象Native Object给从内存中卸载清除, The native garbage collector runs the serializer in a mode where we use it to have objects report all references to external UnityEngine.Objects. This is what makes textures that were used by scene1, get unloaded when you load scene2.

Unity中的序列化系统使用C++开发,用来做引擎底层native object类型(textures,animationclip,camera,等等)的数据序列化。序列化发生在UnityEngine.Object层级,整个Object和引用都会被完整的序列化

Editor和运行时序列化的区别

在这里插入图片描述

脚本序列化

属性必须满足被序列化的条件

  • Be public, or have [SerializeField] attribute
  • Not be static
  • Not be const
  • Not be readonly
  • The fieldtype needs to be of a type that we can serialize.

可以序列化的数据类型

  • Primitive data types (int, float, double, bool, string, etc.)
  • Enum types (32 bites or smaller)
  • Fixed-size buffers
  • Unity built-in types, for example, Vector2, Vector3, Rect, Matrix4x4, Color, AnimationCurve
  • Custom structs with the Serializable attribute
  • References to objects that derive from UnityEngine.Object
  • Custom classes with the Serializable attribute. (See Serialization of custom classes).
  • An array of a field type mentioned above
  • A List of a field type mentioned above

不支持的数据类型

  • No support for polymorphism不支持继承
  • 多维数组,不规则数组,字典
  • 只能修改数据结构,使用支持的数据类型进行组合
    Unity提供了可以自定义序列化的方式
  • Use serialization callbacks, by implementing ISerializationCallbackReceiver, to perform custom serialization.我们可以通过ISerializationCallbackReceiver进行扩展

针对序列化的使用建议

  • 尽量减少序列化的数据量
  • 减少重复序列化的数据,比如MonoBehavior中有多个相同的属性,可以放到ScriptableObject中,如果有100份MonoBehaviour则有100份重复的数据,多个MonoBehaviour引用同一份ScriptableObject
  • 避免多层嵌套和循环嵌套

Unity资产管理

在这里插入图片描述

导入Asset Process

将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为资源分配唯一GUID
  • Unity为每个资源自动生成一个.meta文件
  • Unity执行导入资源流程(使用内置的Importer或者开发者自定义的Importer),在Library文件中生成引擎使用的Native Asset
    • 将原始文件比如FBX,mp3,Png导入到Unity并生成引擎资产Native Asset Object,Unity引擎通过定义的Native Importer将原始文件转化为引擎资产,Unity引擎提供了一些内置导入器
    • Png -> TextureImporter->Native Texture
    • Mp3-> AudioImporter -> Native Audio
    • FBX-> Model Importer-> Native Mesh,Animation,Material
    • 通过Importer定义一个从外部导入到Unity工程的原始资源是被如何解析,生成Native Object Asset的过程,一个FBX可能会生成多个Native Asset(Mesh,Animation,Material)等等,MainAsset,SubAssets
      在这里插入图片描述
      Unity内部针对不同的原始文件类型定了特定导入器
      在这里插入图片描述
      Unity提供了可扩展的Scripted Importer,定义一个外部的原始文件导入到Unity工程中的行为,开发者可以自定义导入器,比如针对扩展名为.cube或者.gltf的原始文件,开发者可以自定义导入到Unity中的逻辑,包括生成prefab,解析,创建文件夹,比如解析GLTF并生成Prefab

Main-Assets和 Sub-Assets

Unity中的Asset可以包含多个SubAssets,比如一个FBX在导入到Unity中可能存在多个Sub-Asset,Material,Animation,Avatar,Model
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

资产的导入管线Hook,AssetPostprocessor

针对Unity导入原始资产生成引擎Asset的过程,开发者可以Hook到对应的流程

  • OnPreprocessModel
  • OnPostprocessMode
  • OnAssignMaterialModel

一般通过AssetPostprocessor做资产的检查和ImportSettings的设置,比如常见的设置

  • UI图片导入关闭mipmap
  • fbx导入关闭isReadable
  • 贴图纹理尺寸限定在1024
  • Apply项目制定的资产导入规则,TextureImporter,ModelImporter等等

The Asset DataBase

Origin Asset 转化为 Unity引擎运行时加载的Native Object Asset格式,AssetDatabase用来管理被转换过后的Native Object Asset

  • Source Assets,跟踪当前原始文件状态,更新时间,文件hash,GUIDS
    • Library\SourceAssetDB
  • Artiface Assets DataBase记录每个Source Asset导入结果
    • Library\ArtifactDB
  • 通过AssetDataBase API来访问Unity资源在这里插入图片描述

Metadata资源元数据

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: 0userData: assetBundleName: assetBundleVariant: fileFormatVersion: 2
guid: db164ed4ea6391b40a3c1649b3653d1e
ModelImporter:serializedVersion: 21300internalIDToNameTable: []externalObjects: {}materials:materialImportMode: 0materialName: 0materialSearch: 1materialLocation: 1animations:legacyGenerateAnimations: 4meshes:userData: assetBundleName: assetBundleVariant: 

.meta文件中包含三个信息:

  • fileFormatVersion:资源版本号,基本上不会发生变化
  • File GUID:内部用来做资源索引,保证资产在移动,重命名的时候,保持引用关系,如果meta文件被删除,资源位置并没有发生变化,Unity会重新生成meta文件并保证GUID不会发生变化,保证引用关系是正常的
  • XXImporter:Source Asset资产的导入设置,每一种Importer都有自己的导入设置和参数
    • 根据导入资源类型不同,AssetImportSetting有不同的导入信息
    • FBX FBXImporter,额外的导入参数
    • PNG TextureImporter,额外的导入参数
    • Assetbundle 名称
    • 等等

由于Meta文件处理不当,可能会造成引用丢失问题:

  1. 在UnityEditor之外做文件移动和重命名,所有的移动或者重命名操作都应该在UnityEditor里面操作,这样Unity会正确的处理好引用关系
  2. 引用关系中的GUID对应的文件,在整个项目中无法找到,就是引用丢失
  3. GUID冲突,如果发现GUID冲突也有可能会造成引用丢失

Asset和实例对象UnityEngine.Objects的区别

在分析Unity的资源管理模块的时候,需要区分Asset和Object的区别

  • Asset:是存储在本地文件中的可见资产,ProjectName/Assets文件夹中的可见的资产(Textures,Model,audio,material,shader等等.asset文件)
  • 一个UnityEngine.Object或者多个UnityEngine.Object组成的一组序列化数据共同描述了Asset的实例,比如Mesh,sprite,AudioClip,AnimationClip,所有的Objects都是继承自UnityEngine.Object,Unity引擎对象。比如我们Resources.Load一个GameObject,并不能直接的使用,需要执行一次实例化操作才能得到可以使用的GameObject
  • 判断一个Object是否是Asset,可以使用AssetDataBase.Contains,返回true表示Object是一个Asset,在AssetFolder中找到对应的Source文件,返回false表示Object是scene中的对象或者是运行时动态创建的Object
  • Asset和Object是一对多的关系,一个Asset可能包含了多个序列化的Object

几乎所有的Object类型都是内置的,Unity中有两个特殊的类型:

  1. ScriptableObject,给开发者提供了一种可以自定义数据结构的方式,Unity可以直接序列化和反序列化ScriptableObject对象,并且可以在Editor Inspector Window进行序列化展示,可以用来存放游戏运行时或者编辑器共享的数据
  2. MonoBehavior,是链接到MonoScript的封装器,MonoScript是底层的数据类型,Unity用来引用到一个实际的脚本对象,MonoScript只是做引用,并不包含具体的代码,MonoBehavior脚本也是一个Asset资源,引用MonoBehavior脚本也是用fileID,guid来实现,每个Prefab添加了对应的MonoBehavior脚本,该Prefab只会有对MonoBehavior脚本的引用关系,并不包含源代码
  MonoBehaviour:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}m_GameObject: {fileID: 8002146119907625182}m_Enabled: 1m_EditorHideFlags: 0// 引用一个继承自MonoBehavior的脚本m_Script: {fileID: 11500000, guid: 96e7330c395831247b6d283dfd987152, type: 3}m_Name: m_EditorClassIdentifier: bConsolePrinting: 1bSGJDebug: 0

Unity中的资源寻址

个人理解Unity资源管理中最核心的部分就是“寻址”,需要了解Unity中如何定义引用关系,内部引用,快文件外部引用,底层是如何实现加载一个Object,如何根据一个Instance ID能够从“外部存储空间”中正确的加载Object,为何Unity开发中会出现引用丢失的问题

寻址关系可以简单的理解为,InstanceID <------>FileGUID + LocalID,Unity底层维护这样一个关系映射表,Object是能被正确可寻址的,才能正确的加载ReadObject成功

Unity基于AssetBundle封装的Addressable,翻译成“可寻址”还是很贴切的。

给定某个需要加载的InstanceId,进行寻址(映射关系),寻址成功(映射关系合法),则根据寻址得到的Source源,执行加载,寻址失败,则加载失败。

File GUID+ LocalID

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 进行区分和标识。

YAML解析

--- !u!1 &505266372
GameObject:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}serializedVersion: 6m_Component:- component: {fileID: 505266376}- component: {fileID: 505266375}- component: {fileID: 505266374}- component: {fileID: 505266373}m_Layer: 0m_Name: Planem_TagString: Untaggedm_Icon: {fileID: 0}m_NavMeshLayer: 0m_StaticEditorFlags: 4294967295m_IsActive: 1--- !u!4 &505266376
Transform:m_ObjectHideFlags: 0m_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: 0m_Children: []m_Father: {fileID: 921185474}m_RootOrder: 6m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}m_Childern 子节点m_Father 根节点MonoBehaviour:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}m_GameObject: {fileID: 8002146119907625182}m_Enabled: 1m_EditorHideFlags: 0// 引用一个继承自MonoBehavior的脚本m_Script: {fileID: 11500000, guid: 96e7330c395831247b6d283dfd987152, type: 3}m_Name: m_EditorClassIdentifier: bConsolePrinting: 1bSGJDebug: 0--- 父节点GameObject--- !u!1 &921185473
GameObject:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}serializedVersion: 6m_Component:- component: {fileID: 921185474}m_Layer: 0m_Name: Framem_TagString: Untaggedm_Icon: {fileID: 0}m_NavMeshLayer: 0m_StaticEditorFlags: 0m_IsActive: 1
--- !u!4 &921185474
Transform:m_ObjectHideFlags: 0m_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: 0m_Children:- {fileID: 1887623492}- {fileID: 1925342256}- {fileID: 505266376}- {fileID: 868233995952783049}- {fileID: 267251194}m_Father: {fileID: 1405857324}m_RootOrder: 2m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}所有的层级关系都会记录到YAML文件中

“— !u!{CLASS ID} &{FILE ID}” which can be analyzed in two parts:(头部信息包含了两个部分)

  • !u!{CLASS ID}: This tells Unity which class the object belongs to. The “!u!” part will be replaced with the previously defined macro, leaving us with “tag:unity3d.com,2011:1” – the number 1 referring to the GameObject ID in this case. Each Class ID is defined in Unity’s source code, but a full list of them can be found here.Unity中对应的CLASS ID列表
  • &{FILE ID}: This part defines the ID for the object itself, which is used to reference objects between each other. It’s called File ID because it represents the ID of the object in a specific file(表示该Object在当前文件中的位置)
  • CLASS ID + FILE ID + OBJECT TYPE

有些特殊的情况,开发者可以自己直接修改YAML文件,比如修改一个带有动画的节点名称,可能造成动画曲线对修改名称过后的节点无法驱动,因为AnimationCurve底层使用PathName来做的映射,这种情况下,我们可以手动修改Animation File的path来实现Rename节点的操作,保证动画能够正常播放
动画系统通过string-based 路径来识别对应的节点,而不是通过GameObject IDs

Path property of Animation Curve
Original YAML and hierarchy on left, renamed GameObject version on right

跨文件索引

Reference to another file object
Reference to another file object

  • GUID:File ID找寻到Asset File(File GUID)
  • File ID:Local ID,定位到AssetFile内部的Object(LocalFileID)
  • Type:
    • Type 2: Assets that can be loaded directly from the Assets folder by the Editor, like Materials and .asset files
    • Type 3: Assets that have been processed and written in the Library folder, and loaded from there by the Editor, like Prefabs, textures, and 3D models

MonoBehaviour YAML referencing a Script asset
Unity的脚本也是当作Asset来处理的,MonoBehaviour引用一个真正的Script资源

依赖引用关系可以编写的工具(不限于)

  1. 查找一个Asset被引用情况,根据Asset 的GUID在其他的Asset YAML中查找GUID,确定Asset被引用的情况
  2. 替换GUID,将一个Asset替换为另外一个Asset
  3. 替换有不同后缀名的Asset,比如将Mp3文件替换为Wav文件,删除Mp3文件,重命名meta文件
  4. 查找引用丢失的Asset,将引用的丢失Asset GUID替换为新的GUID(删除或者重新添加相同的资产)

Instance ID

随着运行时维护的资源增多,通过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。

Loading large hierarchies加载复杂层级对象

游戏对象层级Hierarchy会被完整的的序列化,所有的GameObject和Component都会被序列化到数据中,所以如果Hierarchy层级越复杂,序列化数据会越大,导致Unity在加载并建立游戏对象的Hierarchy有一定的性能开销。
在创建GameObject游戏对象Hierarchy的时候,Unity需要做的几项工作:

  1. 读取Source源(外部存储空间,AssetBundle,Resources,或者其他的GameObject)
  2. 创建新的Transfroms组件,并建立父子节点关系
  3. 实例化GameObjects和Components
  4. 在Unity主线程Awake所有实例化出来的GameObjects和Components
    2,3,4三个步骤,不管GameObject是从已经加载过的GameObject或者从内存中实例化耗时基本相同,但是第一步从Source读取的时间和Hierarchy的复杂度成正比。在实例化一个GameObject对象的耗时,可能会在IO处理上,需要花大量的时间将序列化数据读取出来。

典型案例和优化

一个UI Prefab,包含了30个相同的元素,那么序列化数据将会包含30遍相同元素的序列化数据。生成大量的二进制序列化数据。在加载的时候也是一样会读取和反序列化30遍相同的元素。这样会比较耗费时间。

  • 简化Hierarchy,降低序列化数据大小,not create large blob of binary data
  • 简化Hierarchy,比如Unity提供的Optimize Game Objects选项,隐藏节点,选择对外暴露的节点(Rig Settings)
  • 简化Hierarchy复杂度,尽量避免重复数据,将重复的数据抽离到公用的数据结构中比如ScriptableObject
  • 实例化一个GameObject的时候,采用可以指定Transform Parent的重载API,将多个操作放到同一个API调用中
    • Instantiate(Object original, Transform parent)
    • 在实例化的时候指定父节点Transform,指定旋转和位置
    • 避免先实例化然后单独指定父节点Transfrom,旋转和位置

Unity提供了多种运行时资源管理方案,一种是Resources文件夹模式,一种是AssetBundle,接下来一一介绍这两种方案

Resources资源文件夹(运行时加载)

将资源文件放到Resources文件夹中,运行时可以通过Resources API动态的加载和卸载其中的Asest资源。
在这里插入图片描述

不建议

  1. 使用Resources文件夹会增大APP启动和加载时间,增大APP包体大小
  2. 难以在运行时进行细粒度的内存管理,整体打包成一个序列化文件
  3. 不能热更新资源,不能做资源定制化管理,需要重新打包APP,不支持DLC(downloadable content)

建议

  1. 快速开发迭代,制作游戏原型
  2. 以下情况可以考虑使用Resources文件夹
  3. 资源文件不是内存敏感的,占用内存比较少
  4. 在游戏运行期间一直需要的资源文件
  5. 没有热更新需求的资源文件,全平台通用
  6. APP或者游戏启动时最小数据的配置文件,启动配置相关的

序列化

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并不会在一开始就全部用到。

AssetBundle基础(运行时加载)

继续介绍下,Unity提供的另外一种资源加载方案Assetbundle

什么是Assetbundle

Assetbundle本质上是一个包含了各种Game Ready的资源存档文件(models,贴图,prefab,audio clip,场景等等),本质上就是一个二进制数据存档文件Archive File,在PersistentManager中按照SerializedFile形式保存)参考下文PersistentManager中的讲解

Normal Assetbundle
Scene Assetbundle

优势

  • DLC (downloadable content)支持,资源分发
  • 相比较Resources文件夹,AssetBundle可以做到更细节粒度的内存管理和资产管理
    • 内存粒度可控
    • 资产粒度可控
  • 减少安装包体大小
    • 运行时下载
    • 一边玩一边下载
  • 多平台,多设备,细粒度资源分级管理
    • 提供多套Assetbundle变体适配多平台设备
    • 中高低档资源包
    • 更细粒度的资源内存管理,灵活性高
  • 热更新
    • 资源热更新
    • 逻辑脚本资源热更新(Lua)

数据结构

Header + Data Segment

Header

Assetbundle头部信息,包含标识符identifier,压缩类型compression type,manifest内容清单,manifest是一个以Object资源名称为Key的查找字典,每个字典元素确定了当前Object在data segment中的字节索引位置。用来加载Object。在大多数的平台中,查找字典底层是一个平衡二叉搜索树,Windows和OSX-derived 平台(IOS)使用的是红黑树,查找字典的运行时构建和当前Assetbundle的资源量成正比,资源越多,查找字典构建越耗时。

Data Segment

在这里插入图片描述
Data Segment是Assetbundle数据部分,包含了真正的Object对象集合数据,Unity提供了三种AssetBundle压缩方式:

  • LZMA压缩格式(stream-based-block):所有的资源做整体压缩,压缩率高,加载速度慢,需要将整个文件bundle进行解压,因为所有的资源整体压缩
  • LZ4压缩格式(chunk-based-block):多个Asset压缩到一个Chunk中,在加载Asset的时候,不需要将整个bundle都做解压,chunk-based压缩算法,加载一个Object只需要解压和处理对应的chunk数据即可,压缩率相对LZMA不高,但是加载速度较快,内存友好.WebGL只支持LZ4的压缩格式,LZ4+LocalFromFile一般就是APP侧最合理的加载AssetBundle对的方式了
  • 未压缩:数据部分就是一个原始二进制字节流,加载速度最快,bundle文件最大,

内存占用分析

当加载一个AsesetBundle的时候,Unity会开辟一定的内存空间用来存储AssetBundle数据,加载到内存中通过Memory Profiler查看SerializedFile占用的内存,Unity底层是通过PersistentManager来管理和维护Serialized File对象

CAB-开头的可以理解为从Assetbundle加载的Serialized File
在这里插入图片描述
AssetBundle有以下几点内存占用:

  1. Loading Cache,默认是1MB
    在这里插入图片描述AssetBundle.memoryBudgetKB可以设置大小,默认数值是1.0MB,适合大多数的情况,缓存池大小,运行时可能会多次访问一个AssetBundle多个segment,使用缓存可以提高Asset读取速度(CPU的多级缓存机制 )可以通过详细的测试确定项目中合适的大小
  2. TypeTrees(参考下文中TypeTree的详细介绍)
    • 定义每个Class Type的序列化的规则,Per type not per object,数据安全和版本 data safety and versioning(A TypeTree describes the field layout of one of your data types.)定义成员的内存布局。
    • 解决一个对象的序列化和反序列化规则不一致情况,比如不同Unity版本打包出来的Assetbundle在某些type增加或者删除了某些字段,对于新增的字段可以给默认值比如int给0,保证不会反序列化的时候报错,更好的兼容
    • Typetree不会在每个Assetbundle中共享,所以每一个assetbundle都有一个Typetree信息
    • TypeTree可以通过BuildAssetBundleOptions.DisableWriteTypeTree,在打包AssetBundle的时候不生成TypeTree信息,这样ab包会更小加载速度更快,但是无法做到版本兼容性,加载不同版本的Assetbundle会加载失败,针对特定运行平台必须强制携带类型信息,typetree是无法剔除的,比如WebGL平台,Assetbundle中必须携带TypeTree信息
  3. Table of contents,Assets查找表(Serialized File)
    • Asset根据名称的访问查找表,前文中提到的Indexing Mapping(typedef vector_map<LocalIdentifierInFileType, ObjectInfo> ObjectMap;),根据名称找到对应的ObjectInfo
    • 有了映射关系,我们可以根据ObjectInfo实际的加载Object,实现按名称按需加载,根据名称定位到对应的Asset加载信息,定位到data segment中具体的加载位置
    • 包含的Asset越多,该部分数据占用的内存越大
  4. Preload table预加载表:涵盖了所有的Assetbundle需要涉及到的Object包含引用的完整Object比如如果引用的Object是个GameObject也包含GameObject所有的组件和所有的子节点以及子节点上的组件和组件引用的外部Object(Preloadtable还是很大的一个数据结构),加载Asset的是也是加载该Asset从PreloadTable中引用的startIndex+size关联的对象,从Preload中逐个加载Asset,加载完毕然后返回PreloadTable[startIndex]就是目标加载的Asset,我们可以通过工具查看当前AB包所有的Preload Table内容
    AssetBundleBrower
    PreLoad Table定义了Asset所有依赖的其他资源,加载Asset的时候需要将所有依赖的资源也需要加载,比如一个prefab中的组件可能引用到了其他的Asset,mesh,texture,shader等等。
    Preload table可能会非常的大,比如prefabA(10个Element)和prefabB(5个Element)引用了一个复杂结构的prefabC(100个Element),总共是10 * 100+ 10 * 100个Element,那么preload table会存在两份数据,都包含了完整的prefabC的信息,不管prefabC是否在Ab包中,造成很大的冗余,会发现preload table的size会很大,尽量简化减少asset的引用,简化层级复杂度,比如我们有一个ui_merge_together的AB包,里面包含了所有项目中用到的UI预设,都放到一起,总共有6W多个Element
    在这里插入图片描述

AssetBundle的加载

  • Assetbundle.LoadFromMemory,提供Async异步方法(Unity不建议使用)
    • 内存不友好,占用三分内存,native一份,C#一份,GPU或者Asset本身占用一份内存
  • Assetbundle.LoadFromFile,提供Async异步方法(强烈建议)
    • 速度最快,高效,内存友好
    • 从磁盘或者SD卡中高效加载Assetbundle
    • 未压缩和lz4压缩,可以直接从外部存储读取
    • lzma需要读取过后在内存中做解压,针对lzma不够友好
    • 只会读取Assetbundle的header头信息,只有Object在被请求加载到游戏中才会加载对应的Data segment数据
    • 如果UnityWebRequest有缓存,通过DownloadHandler的方式速度和从LoadFromFile一样快
  • UnityWebRequest’s DownloadHandlerAssetBundle
    • 适用于WebGL或者Ab从远端直接加载的情况
    • 内存友好,不需要额外的内存开销
    • DownloadHandlerAssetbundle,数据下载和处理都在底层Native Code层和工作线程Work Thread中进行。下载并处理完毕,handler并不会在native层持有额外的内存拷贝。降低来了内存开销。
    • lzma压缩的assetbundle,会在下载的过程中进行解压并且使用lz4压缩格式重新压缩并缓存。可以通过Caching.CompressionEnabled控制行为
    • 当下载完毕,Assetbundle可以直接通过DownloadHandler提供的API访问到,如果给UnityWebRequest提供了版本信息,如果已经有缓存,AssetBundle可以被立刻的访问,类似通过AssetBundle.LoadFromFile加载一样
    • 通过Using关键字,保证资源UnityWebRequest被安全的卸载,disposed。
  • WWW.LoadFromCacheOrDownload(on Unity5.6 or older)不建议使用
    • 已经废弃,不建议使用,建议使用UnityWebRequest
    • 内存不够友好,保存双份内存
    • 和UnityWebRequest不同,每次调用WWW都会创建一个新的Worker Thread,造成性能浪费,在内存限制较大的平台上,每次只能保证一个Assetbundle被下载,并发不友好

AB包中的Assets加载

UnityEngine.Object可以通过Unity提供的API从Assetbundle中加载,Unity提供了同步和异步加载方法

  • LoadAsset(LoadAssetAsync)
  • LoaAllAssets(LoadAllAssetsAsync)
    • 适用于将整个AssetBundle中的资源读取出来或者读取一个Assetbundle中大部分的Asset读取,速度较快,一次性读取要比多次调用LoadAsset更快
    • Unity官方的建议,如果从一个AB中加载较大的Asset但是Asset大小占比小于整个AssetBundle的66%,可以将AssetBundle分离到多个AssetBundle并且使用LoadAllAssets
  • LoadAssetWithSubAssets(LoadAssetsWithSubAssetsAsync)

Asset加载底层实现

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加载速度。具体的应该设置为多少,需要根据项目和实际的加载场景设置。

  • ThreadPriority.Low - 2ms
  • ThreadPriority.BelowNormal - 4ms
  • ThreadPriority.Normal - 10ms
  • ThreadPriority.High - 50ms

AssetBundle内存管理

这部分属于老生常谈的知识点了,我们在做一个介绍,AssetBundle.Unload,将加载的Header Information删除,提供的unloadAllLoadedObjects,该参数规定了,AssetBundle被Unload之后对已经加载从AssetBundle中加载的Object如何做处理,而且我们是没有办法将AssetBundle的一部分资源给卸载掉的,只能完整的卸载整个AssetBundle

  • True
    • 从该Assetbundle中加载的Asset都被强制卸载,即使这些Assets还被引用中,比如场景中的游戏对象或者当前场景中还在使用的纹理,模型,动画等等
    • 造成Missing问题,引用丢失,Asset丢失问题
  • False
    • 从该Assetbundle中加载的M Asset不会被卸载。但是当前M Asset和Assetbundle的关联断开,从Remapper删除映射信息(File GUID,LocalID 到InstanceID的映射),当Assetbundle被重新加载,新的Header Information(加载映射信息)会被重新创建,M Asset和重新加载的Assetbundle无任何关联,删除PersistentManager.Remapper中信息
    • 如果M Asset由于某人原因被强制卸载,比如切到后台APP丢失焦点,会导致强制卸载Unity相关的资源,再次回到APP的时候,Unity需要Reload恢复渲染环境,比如M Asset因为对应的Remapper映射已经被清空,无法被寻址,则无法被Reload,导致的现象是:Unload(false)表现正常,但是切到后台再切回来,变成粉红色
    • 如果重新LoadAsset资源M,会创建一个新的M,场景中会存在两个相同的资源M,这种情况会造成“内存泄露”
    • 被断开链接的资源M,被回收只有两种方法(必须触发UnloadUnusedAssets)
      • 消除代码或者场景对资源M的引用,然后手动调用Resources.UnloadUnusedAssets
      • 通过非Addtive方式加载一个场景,Unity会删除当前中所有的游戏对象destroy all objects in curren scene and invoke UnloadunsedAssets,并且调用Resources.UnloadUnusedAssets
      • 系统接收到内存不足警告,主动触发Resources.UnloadUnusedAssets
        请添加图片描述

Unload(false)的问题

  • False:所有被加载的Object不会被销毁,和卸载的AssetBundle断开联系,寻址关系被从映射表中删除(InstanceID<->File GUID+LocalID),可能会造成的三个问题
    • 系统Reload异常:如果Object被强制清空,则Reload的时候无法寻址到Object,则无法完成ReadObject操作,表现粉色或者mesh丢失等,Missing
    • “内存泄露”:永远不会被卸载掉的内存,如果被引用比如Lua中的脚本引用,则永远无法被卸载。这种Object的卸载方式只能在Unity引擎执行UnLoadUnUsedObjects时候被从内存中删除卸载。(参见UnLoadUnUsedObjects的触发时机)
    • 内存重复:已经和AssetBundle断开连接的Object(比如Texture),再次加载相同Object的时候,会重新从AssetBundle加载而不是复用断开连接的Object,因为寻址关系已经被清空,加载名称相同的Object会重新获取新的InstanceId并根据新的InstanceId建立新的寻址关系,IntanceID发生的变化,有造成“内存泄露”的风险

但是并不意味着我们项目中一定都要用UnLoad(True)

AssetBundle使用中需要注意的问题

  1. 管理好Assetbundle在内存中的数量,按照一定的规则即使卸载Assetbundle,虽然Assetbundle一般占用的空间和AB包大小有关,如果Assetbundle较多,那么Assetbundle占用的内存将会比较可观
  2. 如果使用Unload(false)需要注意可能存在内存泄漏的问题,已经断开链接的Asset只能通过触发Resoruces.UnloadUsedAssets卸载
  3. 如果使用Unload(true)需要注意,所有从该AssetBundle中加载的Asset都会被强制的卸载和清空,当前场景或者业务逻辑有依赖这些Asset的都会Missing,具体表现要看Asset的类型,比如粉色,mesh消失,继续脚本访问会直接NullReferenceException异常
  4. 适合项目的资源规划打包规则,关联性
  5. 适合项目的Assetbundle卸载规则

常见的Assetbundle生命周期管理方案

  • 一般都采用引用计数的方案,针对每个Asset做引用计数管理,只有在AssetBundle涉及到的Assets引用计数都为0的情况,才去卸载Assetbundle,这样可以保证Unity在卸载AssetBundle和Reload Assetbundle的时候不会造成Asset重复,内存泄露的问题。
  • 为了能够精细化的控制内存,可以根据使用时机打包Assetbundle,然后使用AssetBundle.Unload(true)强制卸载
  • 有些还会增加对AssetBundle更加细致的管理,比如引用计数为0之后而且10s之后才会去真正的Unload(true)
  • 所有的方案的目的
    • 解决Asset重复加载,内存开销增大
    • 解决内存泄露的问题,运行时的部分Asset永远无法被卸载
    • 提高命中率,提升加载Assetbundle速度,比如引入更加复杂的方案,保证内存的同时,保证最长最近使用的AssetBundle能够更合理的存在游戏内存中

Assetbundle分包策略

不同类型的项目,打包分包规则可能差别会很大

  • 逻辑实体
  • 对象类型
  • 逻辑紧密相关的Object
  • 多数情况一起出现的Objects
  • 需要同时更新的Object
  • 硬件,系统适配,AssetBundle变体

Assetbundle更新方案

  • 在游戏加载执行全部的资源检查和执行更新,更新结束才能进入到游戏
  • 边玩边下
  • 让玩家进入到游戏前,选择需要加载的包,比如高清画质还是低画质的资源包

Unity资源内存管理

在使用Unity引擎开发的过程中我们必须对Unity的资源内存有一定的认知,这样才能尽可能的减少开发过程中带来的内存问题

资源生命周期Resoruce lifecycle

为了更好管理运行时内存,需要了解Asset的生命周期管理,资源卸载和加载的时机。

加载时机

寻址Asset Source源,找得到Object的源地址(File GUID + Local ID)Source location,就可以加载Object,找不到Source加载就会失败
AssetBundle.UnLoad将Header information销毁,加载AssetBundle中的Asset就会寻址失败

可以通过API手动加载Object,比如AssetBundle.LoadAsset等等

一个Object游戏对象被自动加载到内存:

  1. 当前Object对应的指针PPtr被解引用(有引用当前Object的Object被加载)
  2. 当前Object未被加载
  3. 当前Object对应的源地址可以索引到(File ID + LocalID能找到Object所在源位置source location,AB包,Resources)

当一个Object被加载的时候,Unity会检测当前加载的Object所引用包含的所有File ID+LocalID,生成对应的InstanceID,同时执行PPtr解引用操作。

  1. InstanceID对应的Object没有被加载
  2. 通过File ID+LocalID能正确的找到Object所在的Source文件(The Instance ID has a valid File GUID and Local ID registered in the cache)
    如果通过FileID+Local ID找不到对应的Object,该Object将会加载失败,表现的现象就是引用丢失,比如shader丢失表现为粉色,mesh表现为不可见,texture表现为粉红色

卸载时机

  1. 未被引用的Objects被自动从内存中卸载,当Unity的Unused Asset CleanUp发生的时候
    • Unity切换场景的时候会触发(SceneManager.LoadScene非Additive形式)会自动触发Unused Asset CleanUp
    • 脚本调用,Resoures.UnloadUnusedAssets,会触发Unused Asset CleanUp
    • 所有未被引用的Objects会被从内存中卸载,所有HideFlags.DontUnloadUnusedAsset和HideFlags.HideAndDontSave的对象都不会被卸载
    • 在部分系统中如果收到了内存警告也会触发UnloadUnusedAssets
  2. 文件源在Resources中的Persistent,对象可以通过Resources.UnloadAsset进行主动卸载。Instance ID和File ID+Local ID并不会被销毁,如果有PPtr被解引用操作,会重新加载
  3. 文件源在AssetBundle中的Objects,执行了Destroy操作,Unity并不会立刻清空内存,在调用AssetBundle.UnLoad(true)的时候才会从内存中一并和Assetbundle一起卸载,同时FileID+LocalID和Instance ID映射变为不合法,所有从Assetbundle加载的Asset都会被强制卸载,如果仍然引用Asset,会出现Missing的情况,比如mesh不可见,texture粉红色,shader丢失,空引用异常NullReferenceException
  4. AssetBundle.UnLoad(false)如果被调用,已经在内存中的Objects对象不会被从内存中卸载,但是File ID+LocalID和Instance ID的映射变为不合法,如果这些Objects被从内存中卸载,Unity将无法正确的执行Reload操作,因为IDs索引已经被不合法清空。已经和AssetBundle断开关联的Asset只在在系统触发Resources.UnloadUnusedAssets回收
  5. AssetBundle.Unload(false)对应的serilizefile也会从内存中清空(TODO需要做个实验)
  6. AssetBundle是不能被部分卸载的

(File ID+Local ID和Instance ID的映射不合法意味着,Object无法寻址对应的加载源Source(Serialized File),无法加载到内存中,寻址不到Source源。)

如果调用了Unload(false), 映射关系会被清空但是从AssetBundle中加载的Object不会被销毁,后续运行中,Unity如果触发了必须从Assetbundle中Reload资源的情况,会导致加载失败。一种可能会出现的问题的场景如下:

  • Unity失去了对graphic context上下文丢失,移动平台上,APP被强制暂停或者被切换到后台,一般移动端的系统会将清空所有GPU内存,当APP被重新唤醒,Unity需要重新恢复渲染状态,将Textures,Shaders,Meshes资源重新加载到GPU,如果需要Reload的Object,但是Object已经无法寻址,所在的Assetbundle无法找到,就会造成Reload 失败,造成Missing情况,场景粉红色)

内存泄露

已经不会被使用的资源仍然在内存中,一直被引用,导致GC的时候,无法被卸载,比如Lua脚本中如果引用到了C#中的Object,如果没有被正确的处理,很容易造成内存泄露

需要理解的核心基础知识

UnityEngine.Object

  1. UnityEngine.Object,所有Unity引擎相关的资源对象,组件都继承自该Object类
  2. 维护全局的IDToPointerMap全局表,存储当前所有存活的Object对象
  3. 维护ClassID对应的RTTI信息,用来根据ClassID工厂化申请内存并创建Object对象
  4. Object* Object::Produce (int classID, int instanceID, MemLabelId memLabel, ObjectCreationMode mode)创建C++底层Native Object对象

Object的 == 和 != 的重载

public override bool Equals(object o) { return CompareBaseObjects (this, o as Object); } CSRAW private static bool CompareBaseObjects (Object lhs, Object rhs) 
{
#if UNITY_WINRTreturn UnityEngineInternal.ScriptingUtils.CompareBaseObjects(lhs, rhs);
#elsereturn 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;elsereturn lhsInstanceID == rhsInstanceID;
}

PersistentManager,Unity持久化寻址管理器

所有外部加载的Source File(Serialized File)维护,所有Object的读取都必须通过PersistentManager加载,维护寻址关系表Remapper(InstanceID <-> FileGUID + LocalID)

  1. 管理Unity底层持久化资源,StreamContainer,管理所有Unity引擎中加载的SerializedFile,Serialized File可能有不同的来源,比如:
    • From Assetbundle 从AssetBundle加载
    • From Memory 直接从Memory加载
    • From Resources 从Reources加载
  2. 通过Remapper维护资源寻址映射关系InstanceId <-> serializedObjectFileIndex(File GUID)+ localIdentifierInFile (LocalID)(可以理解为根据InstanceId可以定位到该实例对象在外部存储空间或者内存中的位置,可寻址)
  3. 提供接口从持久化的Stream中读取加载Object,一个Object可以被正确加载Read的前提(可寻址),不可寻址表示Object无法找到Source源(Serialized File Stream)无法加载

Profiler中对应的持久化数据源类型(Serialized File)

一个Object可寻址,表示从SerializedFile中能找到对应的Source源,我们可以使用Profiler中查看当前加载的所有Serialized File信息
在这里插入图片描述

  • archive:/CAB-guid: 来自Assetbundle的Source资产源(Serialized File Stream)
    • .sharedAssets:场景Assetbundle额外的资产源
  • globalgamemanagers: global Project Settings data for the player,APP用到的全局配置,全局Manager序列化文件
  • resources.assets: Resoruces文件夹中的所有资源,如果Resources文件中有依赖Resources之外的资源也会包含到resources.assets中
  • Library/unity default resources: Unity内部默认资源,打包产物中对应的unity default resources
  • Resources/unity_built_extra: Unity内置额外资源

!!!Object加载流程底层实现

  1. 当一个PPtr解引用,执行Object加载(参见下文中的PPtr介绍)
    operator T* () const;T* operator -> () const;T& operator * () const;
  1. 检测当前InstanceId对应的Object是否已经被加载,从Object类维护的全局ms_IDToPointer映射表中检查,底层实现
// 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);
}
  1. 如果在全局内存中不存在输入的InstanceID则调用PersistentManager尝试从“外部存储Serialized File”中加载Object
  2. PersistentManager:进行寻址,InstanceId确定是否可寻址到Source Serialized File(InstanceID 到 FileID + Local File ID)是否合法
  3. PersistentManager:如果是合法寻址,找到对应的Source Serialized File Stream,调用ReadObject方法,执行读取
    • PersistentManager:如果寻址不合法,则Object加载失败
  4. PersistentManager:根据拿到的Serialized File执行调用SeriaFile.ReadObject方法,isPersistent = true,可寻址到的Object,而不是动态创建的Object,比如new Mesh,new Texture,new GameObject等等
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 streamSerializedObjectIdentifier 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_LOADINGLogString(Format("--- Loading from main thread failed loading stream %f", (GetTimeSinceStartup () - time) * 1000.0F));#endifm_Mutex.Unlock();return NULL;}// Find file id in stream and read the object// 根据fileid(local id in a file)读取对应的Objectm_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 objectif (o){AwakeFromLoadQueue::PersistentManagerAwakeSingleObject (*o, oldType, kDidLoadFromDisk, didTypeTreeChange, gSafeBinaryReadCallback);}return o;
}
  1. SerializedFile:ReadObject
    • 获取ObjectInfo,获取失败表示当前localFileID在ObjectInfoMap无法找到,return NULL
    • 根据ObjectInfo.ClassID执行Object.Produce操作,根据ClassID申请内存并创建Object架子结构,并将该Object注册到全局Object查找表中,https://docs.unity3d.com/Manual/ClassIDReference.html,
    • 参考下文TypeTree
    • 根据ObjectInfo定义的字节流起始偏移量和大小使用底层填充Object,会执行Unity序列化系统的Transfer方法objectPtr->VirtualRedirectTransfer (readStream),递归调用Transfer,最终完成填充Object并返回
// 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<LocalIdentifierInFileType, ObjectInfo>  ObjectMap;// 读取的ObjectInfo列表,表示当前File所包含的所有Objects信息ObjectMap::iterator iter = m_Object.find (fileID);// 获取ObjectInfo执行创建const ObjectInfo& info = iter->second;// Create empty objectObject* objectPtr = *outObjectPtr;if (objectPtr == NULL){*outObjectPtr = objectPtr = Object::Produce (info.classID, instanceId, kMemBaseObject, mode);}// Type Tree?// 选择合适的Transfer底层序列化工具执行反序列化操作else{#if SUPPORT_SERIALIZED_TYPETREESStreamedBinaryRead<true> readStream;CachedReader& cache = readStream.Init (options);cache.InitRead (*m_ReadFile, byteStart, info.byteSize);Assert(m_ResourceImageGroup.resourceImages[0] == NULL);// Read the objectobjectPtr->VirtualRedirectTransfer (readStream);int position = cache.End ();if (position - byteStart != info.byteSize)OutOfBoundsReadingError (info.classID, info.byteSize, position - byteStart);*didChangeTypeTree = false;#elseAssertString("reading endian swapped is not supported");#endif}
}
  1. PersistentManager:执行Awake Object,在Profiler中会看到这些Awake调用,比如Texture.AwakeFromLoad,Mesh.AwakeFromLoad每种Object类型都会有自己的AwakeFromLoad,并完成相应的路基,比如渲染相关的Object,Mesh,Texture会直接将数据UpLoad到GPU,如果是audio数据,交互代表在主线程准备音频数据并准备播放
    // Awake the objectif (o){AwakeFromLoadQueue::PersistentManagerAwakeSingleObject (*o, oldType, kDidLoadFromDisk, didTypeTreeChange, gSafeBinaryReadCallback);}o.AwakeFromLoad (awakeMode);o.ClearPersistentDirty ();

PPtr

InstanceID和Object映射关系,InstanceId<->ObjectPtr,PPtr是一个指向Object对象的指针。需要的时候延迟加载Object对象(解引用),PPtr的Transfer可以理解为寻址注册,映射关系构建,Unity通过InstanceID来记录引用关系,PPtr指向一个Object对象,在执行实例化Instantiate的时候也是需要将PPtr指向的Object进行拷贝操作

PPtr在序列化文件中的数据结构

  • PPtr asset
  • int m_FileID :索引文件File GUID,定位到文件
  • SInt64 m_PathID:文件中的Object ID,定位文件中的具体Object

AssetBundle中表示当前Bundle中所有的Asset指针定义了FileID+LocalFileID,在AssetBundle进行Transfer的时候,遇到PPtr会执行PPtr的Transfer

map m_ContainerArray Arrayint size = 31[0]pair datastring first = "assets/res/character/npc/shop_batai_001.prefab"AssetInfo secondint preloadIndex = 569int preloadSize = 26PPtr<Object> assetint m_FileID = 0SInt64 m_PathID = 1810623846293925203[1]pair datastring first = "assets/res/character/npc/shop_batai_002.prefab"AssetInfo secondint preloadIndex = 434int preloadSize = 31PPtr<Object> assetint m_FileID = 0SInt64 m_PathID = -287100744004624113[2]pair datastring first = "assets/res/character/npc/shop_caidan_001.prefab"AssetInfo secondint preloadIndex = 150int preloadSize = 27PPtr<Object> assetint m_FileID = 0SInt64 m_PathID = -2163428893405175807[3]pair datastring first = "assets/res/character/npc/shop_deng_001.prefab"AssetInfo secondint preloadIndex = 512int preloadSize = 22PPtr<Object> assetint m_FileID = 0SInt64 m_PathID = 495874309500947719                

PPtr类定义

template<class T>
class PPtr
{SInt32  m_InstanceID;#if !UNITY_RELEASEmutable T*          m_DEBUGPtr;#endifprotected: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);// Assignmentexplicit PPtr (int instanceID){m_InstanceID = instanceID;#if !UNITY_RELEASEm_DEBUGPtr = NULL;#endif}PPtr (const T* o)                               { AssignObject (o); }PPtr (const PPtr<T>& o){m_InstanceID = o.m_InstanceID;#if !UNITY_RELEASEm_DEBUGPtr = NULL;#endif}PPtr (){#if !UNITY_RELEASEm_DEBUGPtr = NULL;#endifm_InstanceID = 0;}PPtr& operator = (const T* o)               { AssignObject (o); return *this; }PPtr& operator = (const PPtr<T>& o){#if !UNITY_RELEASEm_DEBUGPtr = NULL;#endifm_InstanceID = o.m_InstanceID; return *this;}void SetInstanceID (int instanceID)     { m_InstanceID = instanceID; }int GetInstanceID ()const                   { return m_InstanceID; }// Comparisonbool 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解引用操作

  • T* PPtr::operator -> () const
  • T& PPtr::operator * () const
  • PPtr::operator T* () const

Object被从外部Load到内存并加载的时机:当前PPtr被“解引用”的时候,会触发从PersistentManager中读取Object,通常情况下只会持有InstanceID,只有真正的被“解引用”访问的时候,才会从持久化管理器中ReadObject加载对应的Object

template<class T> inline
T& PPtr<T>::operator * () const
{// 如果当前Object在运行时IDMap中不存在// 从PersistentManager读取ObjectObject* temp = Object::IDToPointer (GetInstanceID ());if (temp == NULL)temp = ReadObjectFromPersistentManager (GetInstanceID ());#if !UNITY_RELEASEm_DEBUGPtr = (T*) (temp);#endif#if DEBUGMODE || !GAMERELEASET* 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;}#elsereturn *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_EDITORif (id < 0){#if DEBUGMODE//AssertIf(GetPersistentManager ().ReadObject (id));#endifreturn NULL;}#endifObject* o = GetPersistentManager ().ReadObject (id);return o;}
}

PPtr解引用的时机

  • PPtr解引用时机,加载完AssetBundle,初始化AssetBundle会对PPtr做一次解引用,需要将解析AssetBundle所有的AssetInfo,HeaderInfo(Container,PreloadTable,MainAssset,头部信息可能是TreeType相关的信息)
  • PPtr执行从AssetBundle中加载AssetObject,需要对当前AssetInfo定义的Preload(PreLoad Table,startIndex,tableSize)的Object执行解引用操作,将所有的Preload Object加载到内存

PPtr Curve,引用Object的曲线

m_PPtrCurves,引用外部的Object对象,PPtr Curve类型EditorCurveBinding.PPtrCurve,指向Object的动画曲线,比如给一个Image在动画文件中做一个序列帧动画,每一帧都会引用一个外部的Sprite对象

m_EulerCurves: []m_PositionCurves: []m_ScaleCurves: []m_FloatCurves: []m_PPtrCurves:- curve:- time: 0value: {fileID: 0}- time: 0.033333335value: {fileID: 21300000, guid: ff2bba53a71aed5468e208397796e1c9, type: 3}- time: 0.05value: {fileID: 21300000, guid: 8c9de47e31a111d4996d705840dba765, type: 3}- time: 0.06666667value: {fileID: 21300000, guid: 3127dd5fdad8c1148850546ce86c831b, type: 3}- time: 0.083333336value: {fileID: 21300000, guid: 1dc8c048bb3b7be408250bc0263d4169, type: 3}- time: 0.1value: {fileID: 21300000, guid: acb5c2a66fd2211459bf7ec94ab220d2, type: 3}- time: 0.11666667value: {fileID: 21300000, guid: 062d0d1b5a5462046a980205b54b8626, type: 3}- time: 0.31666666value: {fileID: 0}attribute: m_Spritepath: classID: 114// Monobehaviour// Image.csscript: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!74 &7400000
AnimationClip:m_ObjectHideFlags: 0m_CorrespondingSourceObject: {fileID: 0}m_PrefabInstance: {fileID: 0}m_PrefabAsset: {fileID: 0}m_Name: New AnimationserializedVersion: 6m_Legacy: 0m_Compressed: 0m_UseHighQualityCurve: 1m_RotationCurves: []m_CompressedRotationCurves: []m_EulerCurves: []m_PositionCurves: []m_ScaleCurves: []m_FloatCurves: []m_PPtrCurves:- curve:- time: 0value: {fileID: 0}- time: 0.033333335value: {fileID: 21300000, guid: ff2bba53a71aed5468e208397796e1c9, type: 3}- time: 0.05value: {fileID: 21300000, guid: 8c9de47e31a111d4996d705840dba765, type: 3}- time: 0.06666667value: {fileID: 21300000, guid: 3127dd5fdad8c1148850546ce86c831b, type: 3}- time: 0.083333336value: {fileID: 21300000, guid: 1dc8c048bb3b7be408250bc0263d4169, type: 3}- time: 0.1value: {fileID: 21300000, guid: acb5c2a66fd2211459bf7ec94ab220d2, type: 3}- time: 0.11666667value: {fileID: 21300000, guid: 062d0d1b5a5462046a980205b54b8626, type: 3}- time: 0.31666666value: {fileID: 0}attribute: m_Spritepath: classID: 114// Monobehaviour 114// Image.cs fe87c0e1cc204ed48ad3b37840f39efcscript: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}m_SampleRate: 60m_WrapMode: 0m_Bounds:m_Center: {x: 0, y: 0, z: 0}m_Extent: {x: 0, y: 0, z: 0}m_ClipBindingConstant:genericBindings:- serializedVersion: 2path: 0attribute: 2015549526script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}typeID: 114customType: 0isPPtrCurve: 1pptrCurveMapping:- {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: 2m_AdditiveReferencePoseClip: {fileID: 0}m_AdditiveReferencePoseTime: 0m_StartTime: 0m_StopTime: 0.33333334m_OrientationOffsetY: 0m_Level: 0m_CycleOffset: 0m_HasAdditiveReferencePose: 0m_LoopTime: 1m_LoopBlend: 0m_LoopBlendOrientation: 0m_LoopBlendPositionY: 0m_LoopBlendPositionXZ: 0m_KeepOriginalOrientation: 0m_KeepOriginalPositionY: 1m_KeepOriginalPositionXZ: 0m_HeightFromFeet: 0m_Mirror: 0m_EditorCurves: []m_EulerEditorCurves: []m_HasGenericRootTransform: 0m_HasMotionFloatCurves: 0m_Events: []

TypeTree

定义一个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进行解析,会导致解析报错。

  • static char const* kIncompatibleScriptsMsg = “The asset bundle ‘%s’ could not be loaded because it references scripts that are not compatible with the currently loaded ones. Rebuild the AssetBundle to fix this error.”;
  • static char const* kIncompatibleRuntimeClassMsg = “The asset bundle ‘%s’ could not be loaded because it contains run-time classes of incompatible version. Rebuild the AssetBundle to fix this error.”;
  • static char const* kIncompatibleRuntimeMsg = “The asset bundle ‘%s’ could not be loaded because it is not compatible with this newer version of the Unity runtime. Rebuild the AssetBundle to fix this error.”;

AssetBundle中有TypeTree信息,可以进行比对Class哪些字段的类型变更或者字段增加删除,新增的给默认值,删除字段也给默认值,做到保证读取数据安全和做到版本兼容

TypeTree不能共享,每个AssetBundle必须包含自己的TypeTree数据,这样就会造成信息冗余,内存有额外的开销,而且TypeTree是根据Class type定义的,而不是根据每个Object来定义

不少大厂都针对TypeTree做了引擎级别的定制优化,如果没有版本兼容问题,TypeTree可以通过BuildAssetBundleOptions.DisableWriteTypeTree,在打包AssetBundle的时候不生成TypeTree信息,这样ab包会更小加载速度更快(直接读取并反序列化Transfer不用创建TypeTree,检测是否合法并做修正),但是无法做到版本兼容性,加载不同版本的Assetbundle会加载失败,针对特定运行平台必须强制携带类型信息,typetree是无法剔除的,比如WebGL平台,Assetbundle中必须携带TypeTree信息

ClassID

  1. ClassID定义了和Unity中引擎Class和ID映射关系,通过ClassID能够确定是引擎内部哪一个具体的类
    • 0:Object
    • 4:Transform
    • 23:MeshRender
  2. Unity引擎在Object中维护ClassID对应的RTTI信息,根据ClassID获取对应的Class创建器,根据ClassID工厂化申请内存并创建ClassID对应的Object对象,每个Unity底层继承自Object的对象都需要执行注册
    • REGISTER_DERIVED_CLASS
    • REGISTER_DERIVED_ABSTRACT_CLASS
    • *outObjectPtr = objectPtr = Object::Produce (info.classID, instanceId, kMemBaseObject, mode);
  3. 如果对引擎代码进行裁剪Strip Engine Code,可能在运行时会出现某个ClassID无法被创建Produce的错误,Could not produce class with ID xx,This could be caused by a class being stripped from the build even though it is needed,Try disabling ‘Strip Engine Code’ in PlayerSettings,可以通过关闭Strip Engine Code解决或者将Produce创建失败的ClassID,在link.xml进行显示的声明在这里插入图片描述
 <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做了哪些事情

可以概括的在调用Object.Instantiate的时候,Unity分三个步骤Produce,Copy,Awake,如果在Profiler中开启Deep Profile模式,可以看到在实例化的时候,Unity底层具体做了哪些事情,可以简单的概括为以下几个详细步骤:

  • 创建实例化的Object层级关系骨架,如果是一个GameObject对象递归创建所有的子节点和子节点组件,如果是单个Object直接执行创建操作,不做子节点和子节点组件的递归操作(Creates a gameobjects/components hierarchy same as original object - Instantiate.Produce.
  • TempRemapTable填充映射表typedef vector_map<SInt32, SInt32, std::less, STL_ALLOCATOR(kMemTempAlloc, IntPair) > TempRemapTable;用来后续做内容拷贝,remappedPtrs->insert(make_pair(singleObject.GetInstanceID(), clone.GetInstanceID()));
  • 根据TempRemaptable映射表,读取Origin的数据,然后将数据反序列化到Clone对象上,通过PPtr的Transfer进行序列化和反序列化,PPtr(Copies all fields of all objects from the original objects - Instantiate.Copy
  • 激活Clone对象上的所有的Component并调用Awake函数(Calls Awake on all scripts of new objects Instantiate.Awake.)AwakeAndActivateClonedObjects
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);
#elseBlockMemoryCacheWriter cacheWriter (kMemTempAlloc);
#endif// 执行Clone数据操作// Origin Key:源PPtr<Object>序列化到writeStream中// Clone Value:将数据反序列到Cloned PPtr<Object>上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 DataObject& 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 referencesclone.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 389317CollectAndProduceGameObjectHierarchy(*go, go->QueryComponent(Transform), remappedPtrs);}elseCollectAndProduceSingleObject(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;}
}

参考资料

  1. https://learn.unity.com/tutorial/assets-resources-and-assetbundles(必须要看的文章,本文大部分来自对该文章的解读)
  2. https://blog.unity.com/engine-platform/serialization-in-unity
  3. https://www.youtube.com/watch?v=N-HJvfVuKRw (Unity中的序列化)
  4. https://github.com/Perfare/AssetStudio 查看AssetBundle工具

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/160031.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

傅里叶变换和其图像处理中的应用

以下部分文字资料整合于网络&#xff0c;本文仅供自己学习用&#xff01; 一、为什么要在频域进行图像处理&#xff1f; 一些在空间域表述困难的增强任务&#xff0c;在频率域中变得非常普通 滤波在频率域更为直观&#xff0c;你想想嘛&#xff0c;所谓滤波&#xff0c;就是…

Spring Boot Bean 注入的常用方式教程

Spring Boot Bean 注入是一种将依赖对象引入到应用程序组件中的机制&#xff0c;它有助于实现松耦合和可测试的代码。这种注入方式允许我们将依赖关系委托给 Spring 容器来管理&#xff0c;从而提高了代码的可维护性和可读性。Spring Boot 提供了多种 Bean 注入方式&#xff0c…

Linux-CentOS8-Oracle19c 安装详解

Linux-CentOS8-Oracle19c安装图解 文章目录 Linux-CentOS8-Oracle19c安装图解预备1. Oracle19c 安装手册&#xff1a;2. 安装虚拟机&#xff1a;4G内存&#xff0c;2*2核心&#xff0c;30G3. 下载CentOS8镜像。4. 开始准备预安装5. 修改Oracle账户密码6. 修改SELINUX值在文件&a…

AWS SAP-C02教程2--存储资源

存储资源在架构设计中是一个少不了的环节,而在AWS中有不同类型的存储资源,对应会有不同用途不同价格,SAP考试中考察各种存储是少不了,以下是涉及到的存储 目录 1 非结构化存储1.1 EBS(块存储)1.1.1 基本限制1.1.2 类型1.1.3 RAID 配置选项1.1.4 Snapshot1.2 Local Insta…

thinkphp6 入门(8)-- Session

开启Session Session功能默认是没有开启的&#xff08;API应用通常不需要使用Session&#xff09; think\middleware\SessionInit// 添加引用 use think\facade\Session; 赋值 Session::set(name, thinkphp);取值 // 如果值不存在&#xff0c;返回null Session::get(name)…

CSS的布局 Day03

一、显示模式&#xff1a; 网页中HTML的标签多种多样&#xff0c;具有不同的特征。而我们学习盒子模型、使用定位和弹性布局把内容分块&#xff0c;利用CSS布局使内容脱离文本流&#xff0c;使用定位或弹性布局让每块内容摆放在想摆放的位置&#xff0c;让网站页面布局更合理、…

ESP32-IPS彩屏ST7789-Arduino-简单驱动

目的&#xff1a; 使ESP32能够驱动点亮ST7789显示屏 前提条件&#xff1a; ESP32 ST7789 &#xff08;240 x240&#xff0c;IPS&#xff09; 杜邦线 Arduino 过程&#xff1a; 0x00--接线 0x01--驱动&#xff1a; 彩屏驱动库 针对不同的彩屏驱动芯片&#xff0c;常用的 Arduino…

如何实现 Es 全文检索、高亮文本略缩处理(封装工具接口极致解耦)

如何实现 Es 全文检索、高亮文本略缩处理 前言技术选型JAVA 常用语法说明全文检索开发高亮开发Es Map 转对象使用核心代码 Trans 接口&#xff08;支持父类属性的复杂映射&#xff09;Trans 接口可优化的点高亮全局配置类如下真实项目落地效果为什么不用 numOfFragments、fragm…

深度强化学习 第 3 章 强化学习基本概念

本章讲解强化学习的基本概念。第 3.1 节介绍马尔可夫决策过程&#xff08;Markov decision process&#xff0c;简称 MDP&#xff09;&#xff0c;它是最常见的对强化学习建模的方法。第 3.2 节定义策略函数&#xff0c;包括随机策略和确定策略。第 3.3 节分析强化学习中的随机…

element树形控件编辑节点组装节点

需求功能&#xff1a; 编辑树节点&#xff0c;组装节点 <el-scrollbar class"scrollbar-wrapper"><el-tree :data"nodeList" ref"tree" :props"defaultProps" :expand-on-click-node"false"><template slot…

十三、队列的特性

1、队列(Queue)概述 队列可以用于"任务到任务"、"任务到中断"、"中断到任务"直接传输信息。队列涉及内容如下: 怎么创建、清楚、删除队列队列中消息如何保存怎么向队列发送数据、怎么从队列读取数据、怎么覆盖队列的数据在队列上阻塞式什么意思…

计算机毕业设计-开题报告答辩常见问题!Javaweb项目答辩

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

尚硅谷Flink(一)

目录 ☄️前置工作 fenfa脚本 &#x1f30b;概述 ☄️Flink是什么 ☄️特点&#xff08;多nb&#xff09; ☄️应用场景&#xff08;不用看&#xff09; ☄️分层API &#x1f30b;配环境 ☄️wordcount ☄️WcDemoUnboundStreaming &#x1f30b;集群部署 ☄️集…

计算机毕业设计 基于协同过滤算法的白酒销售系统的设计与实现 Javaweb项目 Java实战项目 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

IP 协议的相关特性(部分)

IP 协议的报文格式 4位版本号&#xff1a; 用来表示IP协议的版本&#xff0c;现有的IP协议只有两个版本&#xff0c;IPv4&#xff0c;IPv6。 4位首部长度&#xff1a; 设定和TCP的首部长度一样 8位服务类型&#xff1a; &#xff08;真正只有4位才有效果&#xff09;&#xf…

【LeetCode 算法专题突破】双指针(⭐)

文章目录 前言1. 移动零题目描述代码 2. 复写零题目描述代码 3. 快乐数题目描述代码 4. 盛最多水的容器题目描述代码 5. 有效三角形的个数题目描述代码 6. 三数之和题目描述代码 7. 四数之和题目描述代码 总结 前言 学算法入门必学的一个章节&#xff0c;双指针算法&#xff0…

2023/10/15总结

学习总结 最近开始写项目了&#xff0c;然后写的过程中遇到了跨域问题。 为什么会出现跨域问题 由于浏览器的同源策略限制。同源策略是一种约定&#xff0c;它是浏览器最核心也是最基本的安全功能。如果缺少了同源策略&#xff0c;那么浏览器的正常功能可能都会收到影响。所谓…

雷电模拟器上使用第一个frida(四)第一个HOOK

经过上述三篇&#xff0c;已经可以使用python3.8.10编写代码&#xff0c;利用frida14.2.18和雷电模拟器9.0.60(9)&#xff0c;Android 9交互。 雷电模拟器上使用第一个frida&#xff08;一&#xff09;之安装-CSDN博客 雷电模拟器上使用第一个frida&#xff08;二&#xff09…

【git的使用方法】——上传文件到gitlab仓库

先进入到你克隆下来的仓库的目录里面 比如&#xff1a;我的仓库名字为zhuox 然后将需要上传推送的文件拷贝到你的克隆仓库下 这里的话我需要拷贝的项目是t3 输入命令ls&#xff0c;就可以查看该文件目录下的所有文件信息 然后输入git add 文件名 我这边输入的是 &#x…

windows内网渗透正向代理

内网渗透正向代理 文章目录 内网渗透正向代理1 正向代理图2 环境准备2.1 正向代理需求&#xff1a; 3 网卡配置3.1 【redream】主机3.2 【base】主机双网卡3.3 【yvkong】网卡设置 4 启动4.1【redream】网卡配置&#xff1a;4.2【base】网卡配置&#xff1a;4.3【yvkong】网卡地…