功能基础
引擎内所有可被识别的类型都需要继承自ObjectBase,每一个ObjectBase都具有两个重要字段:GUID与引用计数。GUID代表着这个对象的唯一名字,并且允许通过这个guid来找到该对象,在序列化系统中扮演者重要角色;
需求分析
生命周期管理主要分为两类:
一类是场景对象引用,如场景内的Node/GameObejct或Component。在这些对象中,Component和Component之间通常有着一定的循环引用,当场景卸载时场景内的这些对象会全部被卸载,在场景正常运行时我们也可以手动的去销毁场景中的某些对象。
而另一种就是资产对象引用,如Shader,Material等,对于资产实例来说没有像场景这么一个容器来管理生命周期,并且资产可能会跨多个场景存在,例如一个资产一边在用于生成序缩略图的世界中渲染,一遍在主场景使用,或可能会在场景加载前预热加载等等。对于资产来说资产与资产之间一般很少会存在循环引用,并且因为明确所有权来控制生命周期的情景下,做一个自动引用计数销毁的机制,并且也应该允许在计数还未置0时手动销毁。
根据以上两种生命周期总结出了对应的两种指针需求:
- 对于所有类型的对象来说,都有支持手动销毁的需求,而手动销毁会使指针悬垂,所以需要能把所有引用的指针置空的需求。
- 引用计数为0使自动销毁的需求。
实现
首先是Object,guid就是一个Object的唯一的、可寻址的标识符。在对象的生命周期之内将自己注册到对象管理器之中。
class Object { guid id; Object(guid _id) : id(_id) { ObjectManager::Register(this, _id); } virtual ~Object() { ObjectManager::Unregister(_id); } };
假如我们要将这个对象销毁,可以通过管理器来销毁该对象。
guid anyId = ...; ObjectManger::Destroy(anyId);
如果只是直接将这个对象在内存中删除,那么其他引用就会出现悬垂指针问题。为了解决这个问题采用二级封装指针
struct ManagedPointer { Object* ManagedPtr = nullptr; int Counter = 0; }; template <typename T> struct ObjectPtr { guid Handle; shared_ptr<AddressableObjectPointer> Ptr; ObjectPtr(Object* obj) : ObjectPtr(obj ? obj->id : guid{}) {} ObjectPtr(guid id) : Handle(id) { Ptr = ObjectManager::GetManagedPointer(id); if (!Ptr) { Ptr = ObjectManager::Emplace(id); } } T* operator->() { if (Ptr) return (T*)Ptr->ManagedPtr; throw NullPointerException; } }
Object实例化时构造里调用的Register,会提前初始化好ManagedPointer对象,这样ObjectPtr构造时就可以获取到这个托管指针,另外在一个guid没有加载进内存时,允许Emplace先占位。等之后该占位处的对象加载出来时在可以通过依赖图去通知依赖对象。
让我们看一下ObjectManager的销毁函数大概的样子,通过设置与外部共享指针内的指针为空,来达到防止悬垂的方案。
struct ManagedValue { Object* OriginalObject; shared_ptr<ManagedPointer> Managed; } static unordered_map<guid, ManagedValue> maps; void ObjectManager::Destroy(guid id) { auto it = maps.find(id); if (it == maps.end()) return; it->second.Managed->ManagedPtr = nullptr; delete it->second.OriginalObject; maps.erase(it); }
另外我们还需要资产带有引用计数的功能,所以在ManagedPointer上加了Counter字段,上面ObjectPtr类型是用来手动销毁维护的没有使用,但对资产引用来说就可以使用了。另外这个类应该还要额外的去注意编写移动构造和赋值重载,移动构造不应修改计数,并把右值的指针置空防止析构时减少计数。右值赋值重载除了做到这点外还需要注意自身计数的减少。
template <typename T> struct RCPtr : public ObjectPtr<T> { RCPtr(guid id) : ObjectPtr(id) { Incref(); } RCPtr(T* t) : ObjectPtr(t) { Incref(); } RCPtr(const RCPtr&) { ... } RCPtr(RCPtr&& r) { Handle = r.Handle; Ptr = r.Ptr; r.Ptr = nullptr; } RCPtr& operator=(const RCPtr&) { ... } RCPtr& operator=(RCPtr&& r) { Decref(); Handle = r.Handle; Ptr = r.Ptr; r.Ptr = nullptr; Incref(); return *this; } T* operator->() { ... } void Incref() { if (!Ptr) return; ++Ptr->Count; } void Decref() { if (!Ptr) return; --Ptr->Count; if (Ptr->Count == 0) ObjectManager::Destroy(Handle); } ~RCPtr() { Decref(); } }
用例
ObjectPtr<Object> obj = new Object(rand_guid()); ObjectManager::Destroy(obj); assert(obj == nullptr); static RCPtr<Asset> asset = new StaticMesh(rand_guid()); { RCPtr<Asset> asset1 = asset; ObjectManger::Destroy(asset1); } assert(asset == nullptr);
ObjectPtr包含的对象只用手动销毁方式来管理,为了方便托管应指定所有权,例如Component被GameObject销毁,GameObject被Scene销毁。
RCPtr则支持计数自动销毁与手动销毁,主要用于资产内存的自动释放。如果将RCPtr对象转换为ObjectPtr对象,则会失去计数功能,但依旧保留着对象guid和自动空引用的功能,可以作为RCPtr的弱引用使用。
ObjectPtr与RCPtr这两种指针总共存在着三种状态:NULL、UNLOAD、OBJECT。
- 当Handle为0时,SharedManagedPtr一定为nullptr,状态为NULL,当前RCPtr不代表任何资源,是RCPtr的默认值。
- 当Handle不为0时SharedManagedPtr一定不为nullptr,当SharedManagedPtr指向的裸指针是nullptr时为UNLOAD状态,代表当前RCPtr指向着一个暂时不存在于内存的资源,资源可能从来都没载入过内存,或者载入内存后被卸载。
- 当ManagedPtr不为空且指向的裸指针也不为空,状态为Object,代表该对象可以被正常访问。