BPVM 小食包 #8 - 清理和净化:内存回收技巧 | ebp BPVM 小食包 #8 - 清理和净化:内存回收技巧 | ebp BPVM 小食包 #8 - 清理和净化:内存回收技巧 | ebp
文章

BPVM 小食包 #8 - 清理和净化:内存回收技巧

蓝图类在编译期间不会被删除和重新创建。它们像白板一样被清理和重用。这里介绍使热重载成为可能的聪明技巧。

BPVM 小食包 #8 - 清理和净化:内存回收技巧

本文内容基于Unreal Engine 5.6.0

BPVM 小食包 - 快速蓝图知识投放蓝图到字节码系列的一部分。

重新编译问题

你点击蓝图上的编译按钮。类需要用新的属性、函数和逻辑重新构建。

天真的方法:

1
2
3
4
5
6
7
8
// 删除旧类
delete OldBlueprintClass;

// 创建新类
UClass* NewClass = new UBlueprintGeneratedClass();

// 现在修复引擎中的每个指针...
UpdateMillionsOfPointers(OldClass, NewClass);  // 噩梦!

这将是一场灾难。每个 actor、每个引用、每个指针都会断裂!

白板解决方案

虚幻的聪明技巧:不要删除类。清理它并重用它!

1
2
3
4
5
void CleanAndSanitizeClass(UBlueprintGeneratedClass* ClassToClean)
{
    // 相同的内存地址,相同的指针
    // 只是擦除内容并写入新东西!
}

把它想象成白板:

  • 当你需要写新东西时,你不会扔掉白板
  • 你只是擦除它并再次书写
  • 白板(内存地址)停留在同一个地方!

瞬态垃圾类

但等等——你不能只是删除属性和函数。其他系统可能正在使用它们!

进入 TRASHCLASS:

1
2
3
4
5
6
7
8
9
10
11
// 创建临时垃圾桶
FName TrashName = "TRASHCLASS_MyBlueprint";
UClass* TransientClass = NewObject<UBlueprintGeneratedClass>(
    GetTransientPackage(),  // 特殊的临时包
    TrashName,
    RF_Transient  // 将被垃圾回收
);

// 将旧东西移到垃圾桶
MovePropertiesToTrash(ClassToClean, TransientClass);
MoveFunctionsToTrash(ClassToClean, TransientClass);

这就像为类成员有一个“回收站”!

什么被移到垃圾桶?

所有将被重新生成的东西:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取所有子对象
TArray<UObject*> ClassSubObjects;
GetObjectsWithOuter(ClassToClean, ClassSubObjects);

for (UObject* SubObj : ClassSubObjects) {
    if (ShouldBeSaved(SubObj)) {
        continue;  // 保留特殊对象
    }

    // 移到垃圾桶
    SubObj->Rename(nullptr, TransientClass);
}

垃圾桶将包含:

  • 旧属性(变量)
  • 旧函数
  • 旧组件
  • 旧元数据
  • 基本上除了 CDO 之外的一切!

CDO 保护

类默认对象获得特殊待遇:

1
2
3
4
5
6
7
8
9
10
// 保存旧 CDO(它有用户的默认值!)
UObject* OldCDO = ClassToClean->GetDefaultObject();

// 重命名它以保护它
FName OldCDOName = "BPGC_ARCH_OldCDO";
OldCDO->Rename(*OldCDOName, TransientClass);

// 稍后,重新编译后...
// 从旧 CDO 复制默认值到新 CDO
FBlueprintEditorUtils::PropagateDefaultValueChange(OldCDO, NewCDO);

你的默认值存活是因为 CDO 被保护和复制!

干净的石板

将所有东西移到垃圾桶后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 清空所有数组
ClassToClean->NetFields.Empty();
ClassToClean->ClassReps.Empty();
ClassToClean->FuncMap.Empty();

// 重置所有指针
ClassToClean->Children = nullptr;
ClassToClean->PropertiesSize = 0;
ClassToClean->MinAlignment = 0;

// 清除所有标志
ClassToClean->ClassFlags &= ~BadFlags;

// 类现在是一块空白的石板!

这就像进行出厂重置但保留序列号!

为什么这很重要

1. 指针保持有效

1
2
3
AActor* MyActor = GetActor();
// 重新编译发生...
MyActor->GetClass();  // 仍然有效!相同的内存地址!

2. 热重载工作

1
2
3
4
// 在游戏中,蓝图被重新编译
CleanAndSanitizeClass(BlueprintClass);
RegenerateClass(BlueprintClass);
// 游戏不会崩溃!所有引用仍然有效!

3. 循环依赖解决

1
2
3
// BP_A 引用 BP_B
// BP_B 引用 BP_A
// 两者都可以重新编译,因为地址不变!

垃圾回收魔法

垃圾桶会怎样?

1
2
3
4
5
// TransientClass 标记为 RF_Transient
// 下次垃圾回收...
if (Object->HasAnyFlags(RF_Transient)) {
    delete Object;  // 垃圾被回收!
}

垃圾类在下一个 GC 周期自动消失!

视觉类比

想象翻新房子:

糟糕的方式(新地址):

  1. 拆除房子
  2. 在新位置建造新房子
  3. 更新每个人的地址簿
  4. 转发所有邮件
  5. 更新 GPS 系统

虚幻的方式(相同地址):

  1. 将家具移到仓库(垃圾桶)
  2. 清空内部(清理)
  3. 重建内部(净化)
  4. 搬入新家具
  5. 地址从未改变!

快速要点

  • 蓝图类在编译期间被重用,不是重新创建
  • 旧成员移动到瞬态包中的 TRASHCLASS
  • CDO 被保护以保留默认值
  • 内存地址保持不变(不需要指针修复!)
  • 垃圾被自动垃圾回收
  • 这使得热重载不会崩溃!

回收冠军

下次你在游戏运行时重新编译蓝图而它没有崩溃时,感谢清理和净化系统。它是使虚幻的热重载感觉像魔法的无名英雄!

想要更多细节?

有关完整的清理和净化分解:

下一个:你的蓝图变量如何成为真正的属性!


🍿 BPVM 小食包系列

本文由作者按照 CC BY 4.0 进行授权