news 2026/4/16 20:47:19

Unreal是如何驾驭内存的 第12章 委托、结构体与反射系统的内存表示

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unreal是如何驾驭内存的 第12章 委托、结构体与反射系统的内存表示

第12章 委托、结构体与反射系统的内存表示

本章目标:深入剖析UE委托(Delegate)系统的内存语义——单播/多播/动态委托的数据布局与Lambda捕获;理解UStruct和蓝图生成类的内存模型;揭示反射系统(UClass、FProperty链)的内存开销。


12.1 委托系统概述

UE的委托系统分为四种主要类型,每种有不同的内存特征:

UE 委托类型谱系

静态委托
不可序列化 · 不可绑定蓝图

动态委托
可序列化 · 可绑定蓝图

DECLARE_DELEGATE
单播委托

DECLARE_MULTICAST_DELEGATE
多播委托

DECLARE_DYNAMIC_DELEGATE
动态单播

DECLARE_DYNAMIC_MULTICAST
动态多播


12.2 单播委托——TDelegate

12.2.1 内存布局

// UE5 TDelegate核心存储template<typenameRetValType,typename...ParamTypes>classTDelegate{// 核心成员FDelegateHandle Handle;// 8字节:唯一标识符TDelegateInstanceInterface*Instance;// 8字节:指向实际绑定// 可能还包含Payload存储};// 实际上UE5使用了一种内联存储优化的设计classFDelegateBase{FDelegateAllocatorType::ForElementType<FAlignedInlineDelegateType>DelegateAllocator;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// 值捕获一个int:4字节int32 Val=42;autoCaptureVal=[Val](){};// sizeof = 4// 引用捕获一个int:8字节(指针大小)autoCaptureRef=[&Val](){};// sizeof = 8// 值捕获一个FString:16字节(TArray内存布局拷贝)FString Str=TEXT("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 多播委托——TMulticastDelegate

12.3.1 内存结构

template<typename...ParamTypes>classTMulticastDelegate:publicTMulticastDelegateBase<FWeakObjectPtr>{// 基类存储typedefTArray<TDelegateBase>InvocationList;InvocationList InvocationListArray;// 即:TArray<绑定存储>FDelegateHandle CompactThreshold;// 用于延迟压缩};

TMulticastDelegate

InvocationList (TArray)

Bind1

Bind2

(空)

Bind3

Num = 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 Delegate

12.4.1 基于FName的绑定

动态委托不存储函数指针,而是存储函数名称字符串(FName):

classFScriptDelegate{TWeakObjectPtr<UObject>Object;// 8字节:绑定对象FName FunctionName;// 8字节:函数名称// 总计:16字节};// 动态多播classFMulticastScriptDelegate{typedefTArray<FScriptDelegate>FInvocationList;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字节(16+10×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 对象。

UScriptStruct"FReflectedData"的元数据布局:

  • StructSize: 12
  • PropertiesSize: 12
  • PropertyLink(FProperty链):
    • FFloatProperty("X", Offset=0)
    • FFloatProperty("Y", Offset=4)
    • FIntProperty("Count", Offset=8)
  • SuperStruct: nullptr
  • StructFlags: …其他元信息

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*Prop=PropertyLink;Prop;Prop=Prop->PropertyLinkNext){if(Prop->ContainsObjectReference()){// 记录偏移量到TokenStreamEmitObjectReference(Prop->Offset_Internal,...);}}}// 运行时通过偏移量读取属性值void*PropAddr=(uint8*)ObjectPtr+Property->Offset_Internal;// 直接内存偏移,无需虚函数调用

12.6 蓝图生成类的内存

12.6.1 UBlueprintGeneratedClass

蓝图编译生成的类继承自UClass,但有额外的内存开销:

UClass(C++反射元数据)——固定大小,加载时创建:

  • FProperty链
  • UFunction列表

UBlueprintGeneratedClass——包含UClass的所有数据,外加蓝图特有的额外内存:

  • 蓝图新增属性的FProperty链
  • 蓝图函数的UFunction(含字节码)← 额外内存
  • 蓝图节点编译后的字节码 ← 额外内存
  • 事件图的UFunction ← 额外内存
  • 默认对象(CDO)

12.6.2 蓝图变量的内存

蓝图中定义的变量存储在实例的尾部(通过动态偏移量访问):

区域内容说明
UObjectBase公共头40字节(对象起始)
C++父类成员变量AMyActor自身成员sizeof(AMyActor)
蓝图成员变量区域BP_Health (float)偏移 = sizeof(AMyActor)+0
BP_Name (FString)偏移 = sizeof(AMyActor)+4
BP_Items (TArray)偏移 = sizeof(AMyActor)+20

蓝图成员通过FProperty::Offset_Internal访问,运行时按偏移读写,无需编译期类型信息。

12.6.3 蓝图的内存代价

一个蓝图类的额外内存开销(相比纯C++):

组成部分典型大小说明
UBlueprintGenClass~2-5 KB反射元数据
蓝图函数字节码~1-50 KB取决于图表复杂度
CDO额外变量按需
蓝图变量FProperty每个~150B

1000个不同蓝图类 × ~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 MB
UFunction~20,000~10 MB
FProperty~80,000~16 MB
UEnum~2,000~0.5 MB
UScriptStruct~3,000~1 MB
总计~32 MB

在大型项目中,反射元数据可能占到 30-50 MB

12.7.2 反射元数据的生命周期

引擎启动

CoreUObject初始化

注册C++反射信息
UClass / UFunction / FProperty
通过 IMPLEMENT_CLASS / UHT 生成的代码

蓝图加载

创建UBlueprintGeneratedClass
编译字节码、注册蓝图属性

运行期
反射元数据持续存在,不会被GC
(UClass标记为RF_MarkAsRootSet)

引擎关闭

反射元数据最后释放


12.8 FInstancedStruct——运行时多态结构体

UE5引入的FInstancedStruct允许在运行时持有不同类型的USTRUCT实例:

// FInstancedStruct可以存储任意USTRUCTFInstancedStruct Instance;Instance.InitializeAs<FMyStructA>();// 分配FMyStructA大小的内存// 稍后可以Instance.InitializeAs<FMyStructB>();// 重新分配为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([Ptr=&BigArray](){...});

减少反射开销

// 不需要蓝图访问的变量不加UPROPERTYstructFMyStruct{UPROPERTY()floatImportantValue;// 反射 + GC追踪floatCachedValue;// 无反射开销};

12.10 小结

  1. 静态委托(~32-64字节/绑定)使用函数指针直接调用,性能最优;动态委托(16字节/绑定)使用FName间接查找,支持蓝图和序列化。

  2. Lambda捕获直接影响委托大小——按值捕获大对象会导致深拷贝,应优先捕获引用或指针。

  3. USTRUCT实例不增加开销(与原生struct相同大小),但每个类型会产生一个UScriptStruct元数据对象(~200-400字节)。

  4. 蓝图类在C++基类基础上增加了反射元数据、字节码和动态属性存储。大型项目中反射系统总计可占30-50MB内存。

  5. FProperty::Offset_Internal是连接反射与内存的核心桥梁——GC、序列化、蓝图VM都通过偏移量直接访问对象内存。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 20:46:21

从I2C到SMBus:搞懂新版Spec 3.3,别再傻傻分不清了(附对比表格)

从I2C到SMBus&#xff1a;搞懂新版Spec 3.3&#xff0c;别再傻傻分不清了&#xff08;附对比表格&#xff09; 在嵌入式系统和硬件设计领域&#xff0c;I2C和SMBus这两种看似相似却又各具特色的总线协议常常让工程师们陷入选择困境。特别是在电源管理、温度监控等关键系统中&am…

作者头像 李华
网站建设 2026/4/16 20:33:47

制造业iPaaS系统集成方案:打通数据孤岛,释放智造新动能

一、前言据中国工业报社数智工业研究中心2026年1月发布的“人工智能制造”十大课题&#xff0c;约70%的工业数据未被激活&#xff0c;工业数据面临“技术异构标准割裂组织壁垒安全顾虑”四维叠加的系统性问题。这意味着制造企业的海量生产数据、设备数据、业务数据中&#xff0…

作者头像 李华