转载:https://blog.csdn.net/qq826364410/article/details/102717636
纯C++类销毁
尽量不要使用new、delete方案,可以使用智能指针。智能指针会使用引用计数来完成自动的内存释放。
使用MakeShareable函数可以来转化普通指针为智能指针。
TSharedPtr
UObject类
无法使用智能指针来管理UObject对象。
UObject类采用自动垃圾回收机制。当一个类的成员变量包含指向UObject的对象,同时又带有UPROPERTY宏定义,那么这个成员变量将会触发引用计数机制。
垃圾回收器会定期从根节点Root开始检查,当一个UObject没有被别的任何UObject引用,就会被垃圾回收。可以通过AddToRoot函数来让一个UObject一直不被回收。
;
Actor类
Actor类对象可以通过调用Destroy函数来请求销毁。
UPROPERTY
当需要将一个UObject类的子类的成员变量注册到蓝图中时,只需要借助UPROPERTY宏即可。
UPROPERTY(BlueprintReadWrite, VisibleAnywhere,Category="Object")
UFUNCTION
UFUNCTION(BlueprintCallable, Category="Object")
可选的还有:
BlueprintImplementEvent:表示这个成员函数有其蓝图的子类实现,不应该尝试在C++中给出函数的实现,这会导致错误。
BlueprintNativeEvent:表示这个成员函数提供一个“C++的默认实现”,同时也可以被蓝图重载。需要提供一个“函数名_Implement”的函数实现,放置于.cpp中。
说明符众多,不便在此一一列出,但可以参考下面的链接:
UCLASS说明符列表
UPROPERTY说明符列表
UFUNCTION说明符列表
USTRUCT说明符列表
FPaths::GameDir() 游戏根目录
FPaths::FileExists() 文件是否存在
FPaths::ConvertRelativePathToFull() 转变相对路径为绝对路径
FString dir = FPaths::ProjectPluginsDir(); UE_LOG(LogTemp, Log, TEXT("%s"), *dir);
第一个参数是Log的分类,第二个参数是Log的类型,分为Log,Warning,Error。Log灰色,Warning黄色,Error红色。具体输出内容为TEXT宏,有三种常用符号:
1.%s字符串(Fstring)
2.%d整形数据(int32)
3.%f浮点型(float)
FString str = FPaths::ProjectDir() + TEXT("Setting.xml"); if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*str)) { }
FPlatformFileManager::Get().GetPlatformFile()获得一个IPlatformFile类型的引用,这个接口提供了通用的文件访问接口。
FName 是无法被修改的字符串,不管出现多少次,在字符串表中只被存储一次。
FText 提供本地化支持。
FString 是唯一提供修改操作的字符串类。
图片文件自身的数据是压缩后的数据,称为CompressedData
图片对应的真正的RGBA数据,是没有压缩的,与格式无关,称为RawData
所有的图片格式,都可以抽象为一个CompressedData和RawData的组合。
虚幻源码目录包含四大部分:Runtime,Development,Editor,Plugin
每个部分有包含了很多个模块。
一个模块文件夹包含:Public文件夹,Private文件夹,模块构建文件.build.cs
只有通过XXX_API宏暴露的类和成员函数才能被其他模块访问。
模块名.build.cs (模块构建文件,告知UBT如何配置自己的编译和构建环境)
public文件夹:
模块名.h
private文件夹:
模块名.cpp
模块名PrivatePCH.h (模块预编译头文件,加速代码的编译,当前模块公用的头文件可以放置于这个头文件中,当前模块所有的.cpp文件,都需要包含预编译头文件)
对于游戏模块,引入当前模块的方式是在游戏工程目录下的Source文件夹中,找到工程名.Target.cs文件。修改SetupBinaries函数,添加引入的模块。
对于插件模块,修改当前插件的.uplugin文件,在Modules数组中引入新的模块。
GEngine->Tick:最重要的任务是更新当前的World。无论是编辑器中正在编辑的World,还是游戏模式下只有一个的World。此时所有World中持有的Actor都会被得到更新。
很多任务无法在一次Tick中完成,就会分在多次Tick函数中完成。
《游戏引擎架构》书中,对内存分配方案重点提到两个方面:
1. 通过内存池降低malloc消耗
2. 通过对齐降低缓存命中失败消耗。
在虚幻引擎中,主要使用还是Intel TBB内存分配器提供的scalable_allocator:不在同一个内存池中分配内存,解决多线程竞争带来的无谓消耗;cache_aligned_allocator:通过缓存对齐,避免假共享。
FRunnable
1. 声明一个继承自FRunnable的类FRunnableTestThread,并实现三个函数:Init、Run和Exit。
2. 借助FRunnableThread的Create方法,第一个参数传入FRunnable对象,第二个参数传入线程的名字。
FRunnableThread::Create(new FRunnableTestThread(0), TEXT("TestThread"));
Task Graph任务系统
由于采用的是模板匹配,不需要每个Task继承自一个指定的类FTestTask,只要具有指定的几个函数,就能够让模板编译通过。
GetTaskName:静态函数,返回当前Task的名字
GetStatId:静态函数,返回当前Task的ID记录类型,可以借助RETURN_QUICK_DECLARE_CYCLE_STAT宏快速定义一个并返回。
GetDesiredThread:指定Task在哪个线程执行
GetSubsequentsMode:用来进行依赖检查的前置标记
DoTask:最重要的函数,Task的执行代码
TGraphTask
Std::Thread
C++11的特性
对象初始化分为:内存分配和对象构造阶段。
获取当前UObject对象对应的UClass类的信息,根据类成员变量的总大小,加上内存对齐,然后在内存中分配一块合适的区域存放。
获取FObjectInitializer对象持有的、指向刚刚构造出来的UObject指针,调用以FObjectInitializer为参数的构造函数(ClassConstructor),完成对象构造。
虚幻引擎序列化每个继承自UClass类的默认值,然后序列化对象与类默认对象的差异。节约了大量子类对象序列化后的存储空间。
反序列化:先实例化对应类的对象,然后还原原始对象数据
如果成员变量没有被UPROPERTY标记,不会被序列化。
如果成员变量与默认值一致,也不会进行序列化。
(1).垃圾回收算法
引用计数法
给每个东西保持一个引用计数。用时加1,不用减1。一旦减为0,进行回收。
优点:
引用计数不用暂停,是逐渐完成的。将垃圾回收的过程分配到运行的过程中。
缺点:
指针操作开销,每次使用都要调整引用计数,频繁使用的物品,频繁修改计数是很大的一笔开销。
环形引用,互相引用的对象,锅与锅盖配套,互相引用,导致两者引用计数都是1。但是实际上需要把锅和锅盖一起垃圾回收。
标记-清扫算法
是追踪式GC的一种。追踪式引用计数算法会寻找整个对象引用网络,从应用程序的root出发,利用相互引用关系,遍历其在Heap(堆)上动态分配的所有对象,没有被引用的对象不被标记,即成为垃圾;存活的对象被标记,即维护成了一张“根-对象可达图”。
优点:
没有环形引用问题,即使锅盖和锅互相引用,也可以垃圾回收。
缺点:
必须暂停,执行完垃圾回收算法后,才能继续做其他事情,导致系统有延迟。
如果只是丢掉垃圾而不整理,就会导致可用空间越来越细碎,最终导致大对象无法被分配。
(2).整理内存:
C#中,启用Compact算法,对内存中存活的对象进行移动,修改它们的指针,使之在内存中连续,这样空闲的内存也就连续了,这就解决了内存碎片问题,当再次为新对象分配内存时,CLR不必在充满碎片的内存中寻找适合新对象的内存空间,所以分配速度会大大提高。但是大对象(large object heap)除外,GC不会移动一个内存中巨无霸,因为它知道现在的CPU不便宜。通常,大对象具有很长的生存期,当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,移动大对象所带来的开销超过了整理这部分堆所能提高的性能。
Compact算法除了会提高再次分配内存的速度,如果新分配的对象在堆中位置很紧凑的话,高速缓存的性能将会得到提高,因为一起分配的对象经常被一起使用(程序的局部性原理),所以为程序提供一段连续空白的内存空间是很重要的。
(3).虚幻引擎的智能指针系统
采用引用计数算法,使用弱指针方案来(部分)解决环形引用问题。
问题:往往忘记去判断哪些是强指针,哪些是弱指针,从而导致内存释放的问题。
tips:智能指针系统管理非UObject对象。
UObject的标记清扫算法
UClass包含了类的成员变量信息,类的成员变量包含了“是否是指向对象的指针”,因此具备选择精确式GC的客观条件。利用反射系统,完成对每一个被引用的对象的定位。故采用追踪式GC。
虚幻在回收过程中,没有搬迁对象,应该是考虑到对象搬迁过程中修正指针的庞大成本。
选择了一个非实时但是渐进式的垃圾回收算法,将垃圾回收的过程分步、并行化,以削弱选择追踪式GC带来的暂停等消耗。
是追踪式、非实时、精确式,非渐近、增量回收(时间片)。
垃圾回收函数 CollectGarbage
虚幻的GC入口是CollectGarbage()
COREUOBJECT_API void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge = true);
void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
// No other thread may be performing UOBject operations while we're running
GGarbageCollectionGuardCritical.GCLock();
// Perform actual garbage collection
CollectGarbageInternal(KeepFlags, bPerformFullPurge);
// Other threads are free to use UObjects
GGarbageCollectionGuardCritical.GCUnlock();
}
锁定/解锁
借助GGarbageCollectionGuardCritical.GCLock/GCUnLock函数,在垃圾回收期间,其他线程的任何UObject操作都不会工作,从而避免出现一边回收一边操作导致各种问题。
回收
回收过程对应函数CollectGarbageInternal中的FRealtimeGC::PerfomReachablilityAnanlysis函数,可以看做两个步骤:标记和清除。不过,增加了簇和增量清除,簇是为了提高回收效率,增量清除是为了避免垃圾回收时导致的卡顿。
标记过程:全部标记为不可达,然后遍历对象引用网络来标记可达对象。
清除过程:直接检查标记,对没有被标记可达的对象调用ConditionalBeginDestroy函数来请求删除。
标记过程的实现原理:
全部标记为不可达:虚幻引擎的MarkObjectsAsUnreachable函数就是用来标记不可达的。借助FRawObjectIterator遍历所有的Object,然后设置标记为Unreachable即可。
MarkObjectsAsUnreachable(ObjectsToSerialize, KeepFlags);
遍历对象引用网络来标记可达对象:
FGCReferenceProcessor ReferenceProcessor;
TFastReferenceCollector
ReferenceCollector(ReferenceProcessor, FGCArrayPool::Get());
ReferenceCollector.CollectReferences(ObjectsToSerialize, bForceSingleThreaded);
这里有几个重要的对象TFastReferenceCollector、FGCReferenceProcessor、以及FGCCollector,分别介绍一下。
TFastReferenceCollector:用于可达性分析。
如果是单线程就调用ProcessObjectArray()函数,遍历UObject的记号流(token stream)来查找存在的引用,如果没有记号流,调用UClass::AssembleReferenceTokenStream()函数就是用生成记号流(token steam,其实就是记录了什么地方有UObject引用),用CLASS_TokenStreamAssembled来保存。
如果是多线程,创建几个FCollectorTask来处理,最终还是调用ProcessObjectArray()函数来处理。
UClass::AssembleReferenceTokenStream()函数
如果没有创建token stream,那么就会遍历当前UClass的所有UProperty,对每个UProperty调用EmitReferenceInfo()函数,这是一个虚函数,如果它有父类,那么就会调用父类的AssembleReferenceTokenStream()函数,并把父类添加到数组的前面,最后加上GCRT_EndOfStream到记号流中,并设置CLASS_TokenStreamAssembled来保存。
FGCReferenceProcessor
处理由TFastReferenceCollector查找得到的UObject引用。
如果Object->IsPendingKill()的返回值为true且允许引用消除,那么把Object的引用设置为NULL
否则,调用ThisThreadAtomicallyClearedRFUnreachable()清除不可达标记,标记为可达,如果这个UObject是簇的根,调用MarkReferencedClustersAsReachable函数,把当前簇引用的其他簇标记为可达,当这个UObject簇根不可达,整个簇都会被回收。
基于簇的垃圾回收
其中跟Cluster相关的几个函数在UObjectBaseUtility中,如下图所示:
用于加速Cook后的对象的回收,所以编辑器下不会使用簇来GC。能够作为簇根的为UMaterial和UParticleSystem,基本上所有的类都可以在簇中。当垃圾回收阶段检查到一个簇根不可达,整个簇都会被回收,加速回收的效率,节省了再去处理簇的子对象的时间。
FGCCollector
继承自FReferenceCollector,HandleObjectReference()和HandleObjectReferences()都调用了FGCReferenceProcessor的HandleObjectReference()方法来进行UObject的可达性分析。
清除过程的实现原理:
为了减少卡顿,虚幻增加了增量清除的概念(IncrementalPurgeGarbage()函数),就是一次删除只占用固定的时间片,一段段进行销毁的触发。