第12章 委托、结构体与反射系统的内存表示本章目标深入剖析UE委托Delegate系统的内存语义——单播/多播/动态委托的数据布局与Lambda捕获理解UStruct和蓝图生成类的内存模型揭示反射系统UClass、FProperty链的内存开销。12.1 委托系统概述UE的委托系统分为四种主要类型每种有不同的内存特征UE 委托类型谱系静态委托不可序列化 · 不可绑定蓝图动态委托可序列化 · 可绑定蓝图DECLARE_DELEGATE单播委托DECLARE_MULTICAST_DELEGATE多播委托DECLARE_DYNAMIC_DELEGATE动态单播DECLARE_DYNAMIC_MULTICAST动态多播12.2 单播委托——TDelegate12.2.1 内存布局// UE5 TDelegate核心存储templatetypenameRetValType,typename...ParamTypesclassTDelegate{// 核心成员FDelegateHandle Handle;// 8字节唯一标识符TDelegateInstanceInterface*Instance;// 8字节指向实际绑定// 可能还包含Payload存储};// 实际上UE5使用了一种内联存储优化的设计classFDelegateBase{FDelegateAllocatorType::ForElementTypeFAlignedInlineDelegateTypeDelegateAllocator;uint32 DelegateSize;};12.2.2 不同绑定方式的内存开销// 1. BindRaw — 原始成员函数指针Delegate.BindRaw(this,MyClass::OnEvent);// 存储对象指针(8B) 函数指针(8B) 16字节// 2. BindUObject — UObject成员函数Delegate.BindUObject(MyActor,AMyActor::OnEvent);// 存储UObject弱引用(8B) 函数指针(8B) 16字节// 会检查UObject是否存活防止悬垂// 3. BindSP — TSharedPtr绑定Delegate.BindSP(SharedObj,FMyClass::OnEvent);// 存储TWeakPtr(16B) 函数指针(8B) 24字节// 4. BindLambda — Lambda表达式Delegate.BindLambda([this,CapturedValue](int32 Param){// ...});// 存储Lambda对象大小取决于捕获列表12.2.3 Lambda捕获的内存Lambda的内存大小 所有捕获变量的大小之和对齐后// 空捕获1字节C标准要求非零大小autoEmpty[](){};// sizeof 1// 值捕获一个int4字节int32 Val42;autoCaptureVal[Val](){};// sizeof 4// 引用捕获一个int8字节指针大小autoCaptureRef[Val](){};// sizeof 8// 值捕获一个FString16字节TArray内存布局拷贝FString StrTEXT(Hello);autoCaptureStr[Str](){};// sizeof 16且堆分配了字符串数据// 值捕获this 多个变量autoHeavy[this,Str,Val,SomeVector](){};// ~50字节陷阱按值捕获FString、TArray等拥有堆资源的对象时每次绑定都会触发深拷贝。在高频调用场景中这成为隐性内存压力。12.2.4 内联存储优化UE对小型绑定使用内联存储类似SBO——Small Buffer Optimization当绑定数据不超过内联阈值通常约48-64字节时直接存储在Delegate对象内部无需堆分配超过阈值时才从堆上分配存储空间。大多数常规绑定BindRaw/BindUObject都在内联范围内。12.3 多播委托——TMulticastDelegate12.3.1 内存结构templatetypename...ParamTypesclassTMulticastDelegate:publicTMulticastDelegateBaseFWeakObjectPtr{// 基类存储typedefTArrayTDelegateBaseInvocationList;InvocationList InvocationListArray;// 即TArray绑定存储FDelegateHandle CompactThreshold;// 用于延迟压缩};TMulticastDelegateInvocationList (TArray)Bind1Bind2(空)Bind3Num 4, Max 8可能有空位解绑后的空洞Remove()不立即压缩数组而是标记为无效延迟到Broadcast()时清理或达到 CompactThreshold 时压缩。12.3.2 多播的内存增长// 典型场景Event DispatcherDECLARE_MULTICAST_DELEGATE_OneParam(FOnHealthChanged,float);FOnHealthChanged OnHealthChanged;// 多个监听者绑定OnHealthChanged.AddUObject(Widget1,UWidget::OnHealthUpdate);OnHealthChanged.AddUObject(Widget2,UWidget::OnHealthUpdate);OnHealthChanged.AddUObject(Widget3,UWidget::OnHealthUpdate);// InvocationList增长每个绑定~32-48字节// 内存估算// 100个监听者 × ~40字节 ≈ 4KB// 通常OK但在大量Actor上的大量事件可能累积12.4 动态委托——Dynamic Delegate12.4.1 基于FName的绑定动态委托不存储函数指针而是存储函数名称字符串FNameclassFScriptDelegate{TWeakObjectPtrUObjectObject;// 8字节绑定对象FName FunctionName;// 8字节函数名称// 总计16字节};// 动态多播classFMulticastScriptDelegate{typedefTArrayFScriptDelegateFInvocationList;FInvocationList InvocationList;// 16字节TArray头 N×16字节};12.4.2 为什么用FName存函数名FName(OnDamageReceived)UObject::FindFunctionChecked(FName)UClass::FindFunctionByName(FName)在UFunction链表中按FName查找找到UFunction* → 调用ProcessEvent()优点可序列化存档存储FName可在蓝图中使用。缺点调用开销远大于直接函数指针——需要FName查找、ProcessEvent虚拟机分发。12.4.3 动态 vs 静态委托内存对比单播多播(10个绑定)静态委托~32-64字节~400-640字节动态委托16字节176字节(1610×16)动态委托实例更小只存FName指针但调用更慢间接查找。12.5 UStruct——结构体的内存布局12.5.1 USTRUCT与原生struct的区别// 原生C structstructFNativeData{floatX;// 4字节floatY;// 4字节int32 Count;// 4字节};// sizeof 12字节无额外开销// USTRUCT版本USTRUCT(BlueprintType)structFReflectedData{GENERATED_BODY()UPROPERTY()floatX;// 4字节UPROPERTY()floatY;// 4字节UPROPERTY()int32 Count;// 4字节};// sizeof 12字节实例大小相同// 但 UScriptStruct 元数据额外占用内存12.5.2 关键区别实例 vs 元数据USTRUCT实例大小 与原生struct相同GENERATED_BODY()不增加成员。但引擎会为每个USTRUCT类型创建一个 UScriptStruct 对象。UScriptStructFReflectedData的元数据布局StructSize: 12PropertiesSize: 12PropertyLink(FProperty链):FFloatProperty(X, Offset0)FFloatProperty(Y, Offset4)FIntProperty(Count, Offset8)SuperStruct: nullptrStructFlags: …其他元信息UScriptStruct本身 ≈ 200-400 字节每类型一个非每实例每个FProperty ≈ 100-200 字节。12.5.3 FProperty链——属性的内存元数据classFProperty{// 核心字段FName NamePrivate;// 8字节属性名int32 ArrayDim;// 4字节固定数组维度int32 ElementSize;// 4字节单元素大小EPropertyFlags PropertyFlags;// 8字节属性标志uint16 RepIndex;// 2字节复制索引int32 Offset_Internal;// 4字节在结构体中的偏移量FProperty*PropertyLinkNext;// 8字节链表下一个FProperty*NextRef;// ... 更多元数据// 虚表指针8字节};// 总计基类 ~80 字节派生类如FStructProperty更大12.5.4 内存偏移量的作用FProperty中的Offset_Internal是GC和序列化的关键// GC如何通过反射遍历引用voidUClass::AssembleReferenceTokenStream(){for(FProperty*PropPropertyLink;Prop;PropProp-PropertyLinkNext){if(Prop-ContainsObjectReference()){// 记录偏移量到TokenStreamEmitObjectReference(Prop-Offset_Internal,...);}}}// 运行时通过偏移量读取属性值void*PropAddr(uint8*)ObjectPtrProperty-Offset_Internal;// 直接内存偏移无需虚函数调用12.6 蓝图生成类的内存12.6.1 UBlueprintGeneratedClass蓝图编译生成的类继承自UClass但有额外的内存开销UClassC反射元数据——固定大小加载时创建FProperty链UFunction列表UBlueprintGeneratedClass——包含UClass的所有数据外加蓝图特有的额外内存蓝图新增属性的FProperty链蓝图函数的UFunction含字节码← 额外内存蓝图节点编译后的字节码 ← 额外内存事件图的UFunction ← 额外内存默认对象CDO12.6.2 蓝图变量的内存蓝图中定义的变量存储在实例的尾部通过动态偏移量访问区域内容说明UObjectBase公共头40字节对象起始C父类成员变量AMyActor自身成员sizeof(AMyActor)蓝图成员变量区域BP_Health (float)偏移 sizeof(AMyActor)0BP_Name (FString)偏移 sizeof(AMyActor)4BP_Items (TArray)偏移 sizeof(AMyActor)20蓝图成员通过FProperty::Offset_Internal访问运行时按偏移读写无需编译期类型信息。12.6.3 蓝图的内存代价一个蓝图类的额外内存开销相比纯C组成部分典型大小说明UBlueprintGenClass~2-5 KB反射元数据蓝图函数字节码~1-50 KB取决于图表复杂度CDO额外变量按需蓝图变量FProperty每个~150B1000个不同蓝图类 × ~10KB/类 ≈ 10MB 元数据内存12.7 反射系统的总体内存开销12.7.1 反射元数据组成UE反射系统在内存中的存在每个 UClass ≈ 500-2000 字节每个 UFunction ≈ 200-800 字节每个 FProperty ≈ 100-300 字节每个 UEnum ≈ 200-500 字节每个 UScriptStruct ≈ 200-400 字节典型项目的反射元数据总量类别数量估算内存UClass~5,000~5 MBUFunction~20,000~10 MBFProperty~80,000~16 MBUEnum~2,000~0.5 MBUScriptStruct~3,000~1 MB总计~32 MB在大型项目中反射元数据可能占到 30-50 MB12.7.2 反射元数据的生命周期引擎启动CoreUObject初始化注册C反射信息UClass / UFunction / FProperty通过 IMPLEMENT_CLASS / UHT 生成的代码蓝图加载创建UBlueprintGeneratedClass编译字节码、注册蓝图属性运行期反射元数据持续存在不会被GCUClass标记为RF_MarkAsRootSet引擎关闭反射元数据最后释放12.8 FInstancedStruct——运行时多态结构体UE5引入的FInstancedStruct允许在运行时持有不同类型的USTRUCT实例// FInstancedStruct可以存储任意USTRUCTFInstancedStruct Instance;Instance.InitializeAsFMyStructA();// 分配FMyStructA大小的内存// 稍后可以Instance.InitializeAsFMyStructB();// 重新分配为FMyStructB// 内部结构structFInstancedStruct{constUScriptStruct*ScriptStruct;// 8字节类型信息uint8*StructMemory;// 8字节堆上的实例数据// 总计16字节头 堆上的实例数据};使用场景数据驱动的能力系统、可配置的行为树节点参数等。12.9 实践建议委托选择需要蓝图绑定 → 动态委托DECLARE_DYNAMIC_... 只需C绑定 → 静态委托DECLARE_DELEGATE/MULTICAST 需要序列化 → 动态委托 性能敏感的高频事件 → 静态委托避免FName查找开销Lambda捕获注意事项// ✗ 按值捕获大对象Delegate.BindLambda([BigArray,BigMap](){...});// 深拷贝// ✓ 按引用捕获确保生命周期安全Delegate.BindLambda([BigArray](){...});// 仅8字节指针// ✓ 或捕获指针/引用Delegate.BindLambda([PtrBigArray](){...});减少反射开销// 不需要蓝图访问的变量不加UPROPERTYstructFMyStruct{UPROPERTY()floatImportantValue;// 反射 GC追踪floatCachedValue;// 无反射开销};12.10 小结静态委托~32-64字节/绑定使用函数指针直接调用性能最优动态委托16字节/绑定使用FName间接查找支持蓝图和序列化。Lambda捕获直接影响委托大小——按值捕获大对象会导致深拷贝应优先捕获引用或指针。USTRUCT实例不增加开销与原生struct相同大小但每个类型会产生一个UScriptStruct元数据对象~200-400字节。蓝图类在C基类基础上增加了反射元数据、字节码和动态属性存储。大型项目中反射系统总计可占30-50MB内存。FProperty::Offset_Internal是连接反射与内存的核心桥梁——GC、序列化、蓝图VM都通过偏移量直接访问对象内存。