UE4技術總結——委託
UE4技術總結——委託
在開始前先說明,這部分內容主要基於UE 4.26.2版本[1]編寫,因此讀者可能需要注意時效性。如果您對源程式碼感興趣,可以直接閱讀引擎的DelegateCombinations.h
、Delegate.h
以及相關程式碼。
因為是一個非常基礎,時不時會用到的功能,所以這裡就不介紹使用場景了,直接進入正題。
一、定義
首先,官方定義如下[2]:
委託 是一種泛型但類型安全的方式,可在C++對象上調用成員函數。可使用委託動態綁定到任意對象的成員函數,之後在該對象上調用函數,即使調用程式不知對象類型也可進行操作。複製委託對象很安全。你也可以利用值傳遞委託,但這樣操作需要在堆上分配記憶體,因此通常並不推薦。請盡量通過引用傳遞委託。
同時,根據官方文檔,虛幻引擎支援3種類型的委託:
- 單播委託
- 多播委託
- 事件
- 動態委託
之所以說是3種,是因為事件實際上在現在的版本中差不多就是多播委託(當然,實際上還是有些許不同的,主要是函數調用許可權和多播不同,但是實際上也沒有措施保證函數被不是擁有者的對象調用,因此讀者只需要理解為多播委託即可)[3]。而且在UE的4.26.2版本源碼中已經標明,事件類型的委託將會在後面更新的版本移除掉:
因此,我們主要重點還是放在單播、多播、動態委託上,事件不會進行詳細說明。
同時,UE4中存在由基本委託組合起來的委託,但是在介紹組合的委託之前我們先看看這3種基本委託。
接下來我們先簡單看看該怎麼用。
順帶一提,這裡我默認讀者知道如何在C++中實現委託,如果您還不清楚,那麼建議閱讀文末參考中列出的的文章[4](了解即可)。
二、用法
2.1 聲明與調用委託
UE4中的委託都通過宏定義來聲明,隨後就可以通過宏定義聲明的委託來聲明對應的委託變數,實際使用的時候會通過將函數綁定到委託變數來使用。
2.1.1 單播委託
-
單播委託只能綁定一個函數指針,執行委託的時候也只能觸發一個函數;
-
單播委託綁定的函數可以有返回值,這點和多播委託不同;
2.1.1.a 聲明
// 無返回值函數的委託
// 無參數
DECLARE_DELEGATE(DelegateName);
// 1個參數
DECLARE_DELEGATE_OneParam(DelegateName, Param1Type);
// <num>個參數,最多到9個
DECLARE_DELEGATE_<num>Params(DelegateName, Param1Type, Param2Type, ...);
// 有返回值
// 無參數
DECLARE_DELEGATE_RetVal(RetValType, DelegateName);
// 1個參數
DECLARE_DELEGATE_RetVal_OneParam(RetValType, DelegateName, Param1Type);
// 多個參數,最多到9個
DECLARE_DELEGATE_RetVal_<num>Params(RetValType, DelegateName, Param1Type, Param2Type, ...);
一個簡單的聲明單播委託的例子:
// 直接用宏定義在頂部聲明就可以了
DECLARE_DELEGATE(FLearningDelegate);
class XXX_API ALearnDelegateActor : public AActor
{
GENERATED_BODY()
public:
// ... 省略
public:
// 單播委託帶有UPROPERTY宏,不能添加BlueprintAssignable標識符,動態多播才可以聲明BlueprintAssignable
FTestDelegate FLearningDelegate;
}
2.1.1.b 綁定
在綁定函數之前我們先要聲明委託和委託變數:
// 單播無參數的委託,其他類型的單播委託如此類推
// 這行通常放在頭文件的上方,類定義之外,畢竟是宏
DECLARE_DELEGATE(FSingleDelagateWithNoParam);
// 用上面聲明的委託聲明委託變數
// 這裡放在類定義中,作為一個屬性進行定義
FSingleDelagateWithNoParam SingleDelagateWithNoParam;
然後我們就可以綁定函數了,綁定函數的API有很多種,但是最常用的還是BindUObject
,因此這裡以BindUObject
舉例:
// ADelegateListener::EnableLight的定義類似於void ADelegateListener::EnableLight(),沒有參數,也沒有返回值
// 這個綁定假設是在類裡面綁定的,所以用了this,實際上可以是別的UObject
SingleDelagateWithNoParam.BindUObject(this, &ADelegateListener::EnableLight)
下面這張圖列舉了除了BindUObject
之外還能夠使用什麼函數進行綁定,以及在什麼情況下使用[2:1]。
除了BindUObject
之外還有別的綁定函數,這裡直接借用官網過時的文檔中的列表:
大概如上,都非常簡單,在使用的時候按照您要綁定的函數來選擇對應的函數來綁定即可。這裡簡單補充幾個官網文檔沒有提及的綁定:
函數 | 描述 |
---|---|
BindThreadSafeSP(SharedPtr, &FClass::Function) |
用一個弱指針TWeakPtr 來綁定一個原生C++類成員函數,當指針SharedPtr 指向的對象無效的時候不會執行綁定的回調函數 |
BindWeakLambda(UObject, Lambda) |
綁定一個匿名函數,在傳入的UObject有效,還沒有被回收的時候都可以調用這個匿名函數。這個匿名函數中可以用this ,但是其他關鍵詞不一定能用 |
BindUFunction(UObject, FName("FunctionName")) |
用來綁定一個UObject的UFUNCTION函數,原生的與動態的委託都可以用這個函數來綁定回調函數 |
這裡提幾個注意事項[5]:
- 注意
BindRaw
綁定的普通C++對象的成員函數,要特別注意執行的時候這個對象有沒有被銷毀。如果被銷毀了那麼觸發委託執行綁定的函數會導致報錯; - 注意
BindLambda
綁定的Lambda表達式捕獲的外部變數,如果在觸發委託的時候會導致報錯; BindWeakLambda
、BindUObject
、BindUFunction
綁定時會弱引用一個UObject對象,需要預先IsBound()
或者ExecuteIfBound
來判斷是否該對象還有效再執行委託,否則可能會報錯;BindSP
、BindThreadSafeSP
綁定時會弱引用一個智慧指針對象(UE4的智慧指針),執行前需要先IsBound()
或者ExecuteIfBound
來判斷該對象是不是還存在,否則可能會報錯;- 如果單播委託對象被銷毀,那麼析構函數會自動調用
UnBind
進行解綁;
2.1.1.c 執行委託
執行單播委託需要調用的函數主要是Execute(您要傳入的參數)
,要注意的是,這個函數並不會檢查您的綁定情況,因此如果委託未綁定,那麼直接執行此函數會導致報錯。因此往往推薦在調用Execute(傳入參數)
前先用IsBound()
來檢查是否已經進行了綁定。當然也可以直接調用ExecuteIfBound(傳入參數)
,這個函數等效於if(委託.IsBound())
進行判斷後再執行Execute(傳入參數)
。
2.1.1.d PayLoad
首先介紹下PayLoad的功能,PayLoad是委託綁定的時候傳入的額外參數列表,保存在委託對象內。觸發委託的時候PayLoad會跟著Execute(傳入的參數)
、ExecuteInBound(傳入的參數)
傳入的參數之後填充到綁定函數的參數列表中,然後執行。
舉個例子:
DECLARE_DELEGATE_OneParam(FLearnDelegate, float);
static void LearningDelegate(float Bar) {
UE_LOG(LogTemp, Log, TEXT("=== INFO: FOOO %f ==="), Bar);
}
static void LearningPayload(float Bar, FString Test) {
UE_LOG(LogTemp, Log, TEXT("=== INFO: FOOO %f, %s ==="), Bar, *Test);
}
// 在GameInstance的初始化函數中或者其他地方
// 正常使用
FLearnDelegate DelegateObj1;
DelegateObj1.BindStatic(LearningDelegate);
DelegateObj1.ExecuteIfBound(23.0f);
// PayLoad
FLearnDelegate DelegateObj2;
// 這裡的「TEST」會在調用綁定函數的時候緊接著委託對戲那個傳入的參數傳入
DelegateObj2.BindStatic(LearningDelegate, FString(TEXT("TEST!")));
// 「TEST」會接在23.0f後面,所以最後是傳入到Test參數中
DelegateObj2.ExecuteIfBound(23.0f);
2.1.1.e 底層實現
綁定函數指針
相關程式碼在DelegateCombination.h
以及Delegate.h
中。
首先我們需要有個大體的概念,其實本質上就是保存了一個函數指針,在執行的時候直接訪問該函數指針對應的函數即可,如果是成員函數則比較特殊,需要同時知道成員函數所在的類,同時應該有一個弱指針指向該對象。接下來我們看具體實現。
可以看到,實際上就是通過TDelegate
這個類來實現的,所以實際上我們在定義委託的時候就是在調用TDelegate<returntype(一堆您傳入的參數)>
創建委託類型,並通過typedef
重命名為您給定的名字,方便記憶與閱讀。TDelegate
在DelegateSignatureImpl.ini
中實現。因為內容比較多,因此我們只看關鍵部分。首先我們看到他繼承了TDelegateBase
這個類:
簡單掃幾眼,就會發現實際上用來保存指向函數的指針並不在TDelegate
中,而應該是放在了父類,也就是TDelegateBase
:
讀者可能發現了UserPolicy
這個參數,這裡實際上是4.26版本才新加入的內容[6]:
之前的靜態單播的基類是
FDelegateBase
,這個類沒有變化,但是所有的public
介面被改成了protected
,無法直接使用了。這一點真是非常糟糕,哪有增加可擴展性的同時把介面都藏起來的,本來所有實現就都是寫到頭文件里的。最大的不同是接下來的地方,其實現不是通過直接對
FDelegateBase
的繼承完成的,而是通過一個叫做FDefaultDelegateUserPolicy
的結構體進行中轉的。這個結構體中只定義了三個類型的別名,分別是FDelegateInstanceExtras
、FDelegateExtras
、FMulticastDelegateExtras
。其中FDelegateExtras
指向的就是FDelegateBase
。靜態單播的實現類
TDelegateBase
(原來叫TBaseDelegate
,這詭異的命名)變成了模板類,該類繼承於模板參數中的FDelegateExtras
類型。說到這裡我想應該已經明白了UE4這個改動的含義。這意味著我們可以通過自己定義一個FDefaultDelegateUserPolicy
以外的其他結構體UserPolicy
,並在其中定義上述三個類型,就可以釜底抽薪式地把寫在底層的實現替換成我們自定義的實現,這無疑很大地增加了這個模組的可擴展性。
簡單的說就是FDelegateBase
在經過抽象之後,允許用戶單獨創建一個UserPolicy
結構體給TDelegateBase
來自定義委託,當然如果沒有傳入自己定義的UserPolicy的話,那麼會使用默認的FDefaultDelegateUserPolicy
(這裡用到了C++的模板偏特化特性[7][8],能夠在給定默認值的同時,能夠讓用戶輸入自己希望的值):
因此實際上此處的UserPolicy
是FDefaultDelegateUserPolicy
,那麼我們簡單看看FDefaultDelegateUserPolicy
這一struct
的內容:
struct FDefaultDelegateUserPolicy
{
// 這裡的using是別名指定
using FDelegateInstanceExtras = IDelegateInstance;
// 注意下面這個,另外兩個會在其他委託中用到,先不管
using FDelegateExtras = FDelegateBase;
using FMulticastDelegateExtras = TMulticastDelegateBase<fdefaultdelegateuserpolicy>;
};
回到開始的TDelegate<inretvaltype(paramtypes...), userpolicy=""> : public TDelegateBase<userpolicy>
,我們看看TDelegateBase
的定義:
所以實際上最終還是繼承了FDefaultDelegateUserPolicy::FDelegateExtras
,即FDelegateBase
。
我們繼續追蹤GetDelegateInstanceProtected()
,繼續看TDelegateBase
,但是我們會發現,實際上TDelegateBase
也沒有保存指針,只是提供了一系列函數(如,是否已經綁定了函數的IsBound()
等):
template <typename userpolicy="">
class TDelegateBase : public UserPolicy::FDelegateExtras
{
template <typename>
friend class TMulticastDelegateBase;
// 用using指定別名
using Super = typename UserPolicy::FDelegateExtras;
public:
// 省略部分注釋與宏判斷
FName TryGetBoundFunctionName() const
{
// 注意這裡,可以看出不是這裡保存的函數指針
if (IDelegateInstance* Ptr = Super::GetDelegateInstanceProtected())
{
// 實際上還是調用了委託對象提供的函數來實現具體的功能
return Ptr->TryGetBoundFunctionName();
}
return NAME_None;
}
// 省略一系列函數
}
可以看到,實際上即便是TDelegateBase
,也是要通過Super::GetDelegateInstanceProtected()
來獲取委託對象,這個函數最終調用FDelegateBase
類提供的GetDelegateInstanceProtected()
來獲取委託對象(注意using Super = typename UserPolicy::FDelegateExtras;
,而在FDefaultDelegateUserPolicy
中,using FDelegateExtras = FDelegateBase;
),最終通過IDelegateInstance
類的委託對象提供的函數來實現相關功能。因此我們還得要接著往下面看才能找到真正保存函數指針的地方。
因此,我們看到FDelegateHandle
:
class FDelegateBase
{
template <typename>
friend class TMulticastDelegateBase;
template <typename>
friend class TDelegateBase;
protected:
/**
* Creates and initializes a new instance.
*
* @param InDelegateInstance The delegate instance to assign.
*/
explicit FDelegateBase()
: DelegateSize(0)
{
}
~FDelegateBase()
{
// 可以看到實際上在被銷毀的時候會自動調用函數取消綁定
Unbind();
}
// 省略部分函數
// 這裡是重點
/**
* Gets the delegate instance. Not intended for use by user code.
*
* @return The delegate instance.
* @see SetDelegateInstance
*/
FORCEINLINE IDelegateInstance* GetDelegateInstanceProtected() const
{
return DelegateSize ? (IDelegateInstance*)DelegateAllocator.GetAllocation() : nullptr;
}
// 省略函數
private:
// 這個也是重點
FDelegateAllocatorType::ForElementType<falignedinlinedelegatetype> DelegateAllocator;
int32 DelegateSize;
}
上面提到,TDelegateBase
最終調用的是FDelegateBase
提供的GetDelegateInstanceProtected()
,而這裡我們可以看到,實際上是返回IDelegateInstance
類型的數據(這裡先忽略掉DelegateAllocator
,只需要理解為一個工具類,用來分配記憶體,因為與委託不太相關所以先不詳細說明,如果感興趣可以看這篇文章[9]),因此最終函數指針理論上是包裹在IDelegateInstance
中的。
但是再想想,實際情況肯定沒有這麼簡單,還記得我們前面說到的綁定函數嗎?實際可能傳入的函數指針類型非常多,例如可能傳入一個在UObject
對象中的成員函數,可能傳入一個lambda
函數等。所以實際上,會包裹在IDelegateInstance
為基類的,根據各種傳入函數指針類型進行適配的派生類中。
例如,接著上面往下看,我們可以看到這類型的函數:
/**
* Static: 用來創建C++全局函數指針的委託
*/
template <typename... vartypes="">
UE_NODISCARD inline static TDelegate<retvaltype(paramtypes...), userpolicy=""> CreateStatic(typename TIdentity<retvaltype (*)(paramtypes...,="" vartypes...)="">::Type InFunc, VarTypes... Vars)
{
TDelegate<retvaltype(paramtypes...), userpolicy=""> Result;
// 重點是下面這個,TBaseStaticDelegateInstance的基類就是IDelegateInstance
TBaseStaticDelegateInstance<functype, userpolicy,="" vartypes...="">::Create(Result, InFunc, Vars...);
return Result;
}
/**
* Static: 創建lambda函數的委託
*/
template<typename functortype,="" typename...="" vartypes="">
UE_NODISCARD inline static TDelegate<retvaltype(paramtypes...), userpolicy=""> CreateLambda(FunctorType&& InFunctor, VarTypes... Vars)
{
TDelegate<retvaltype(paramtypes...), userpolicy=""> Result;
TBaseFunctorDelegateInstance<functype, userpolicy,="" typename="" tremovereference<functortype="">::Type, VarTypes...>::Create(Result, Forward<functortype>(InFunctor), Vars...);
return Result;
}
// 還有更多,這裡忽略
簡單看下TBaseStaticDelegateInstance
:
可以很輕鬆找到保存C++函數指針的變數(這個變數類型是UE4提供的專門用來保存C++函數指針的類型,網上資料很多[10],這裡就不進行介紹了)。
同理,相似的,綁定UObject
對象成員函數委託創建函數則有:
最終執行的時候的形式就類似於這樣:
// 全劇函數
(*MyDelegate)();
// 對象成員函數
(MyObj->*FuncPtr)();
// 如果是在棧上
(StackObj.*Func1Ptr)();
Payload的實現
當然實際上UE4中會支援Payload,會先把一部分預先輸入的參數拼接到調用委託的時候傳入的參數後面去,形成一個參數列表,最後一起作為參數輸入到綁定函數,但是原理差不多。
以全局函數的執行為例:
Payload
實際上是一個TTuple
:
最終執行:
因為Payload
特性前面介紹過,所以這裡不贅述。
綁定
但是只有創建是不行的,這時候的委託還沒有綁定上要執行的函數。我們還是以綁定全局函數為例:
/**
* 綁定一個C++全局函數
*/
template <typename... vartypes="">
inline void BindStatic(typename TBaseStaticDelegateInstance<functype, userpolicy,="" vartypes...="">::FFuncPtr InFunc, VarTypes... Vars)
{
*this = CreateStatic(InFunc, Vars...);
}
結合上面的CreateStatic
的實現就可以明白了,因為CreateStatic
返回的是右值,這裡左側的*this
和=
會調用到TDelegate
的Move Assigment Operator:
/**
* Move assignment operator.
*
* @param OtherDelegate Delegate object to copy from
*/
inline TDelegate& operator=(TDelegate&& Other)
{
if (&Other != this)
{
// this down-cast is OK! allows for managing invocation list in the base class without requiring virtual functions
DelegateInstanceInterfaceType* OtherInstance = Other.GetDelegateInstanceProtected();
if (OtherInstance != nullptr)
{
OtherInstance->CreateCopy(*this);
}
else
{
Unbind();
}
}
return *this;
}
最終將創建出來的TDelegate
賦值給自身,從而實現綁定函數。
綁定不同的函數指針對應不同的T<函數指針類型>DelegateInstance<...>::Create(...)
,這裡列舉下,實際上看源程式碼也可以理解:
創建函數 | 對應的Delegate Instance創建函數 |
---|---|
CreateStatic() |
TBaseStaticDelegateInstance<...>::Create(...) |
CreateLambda() |
TBaseFunctorDelegateInstance<...>::Create(...) |
CreateWeakLambda() |
TWeakBaseFunctorDelegateInstance<...>::Create(...) |
CreateRaw() |
TBaseRawMethodDelegateInstance<...>::Create(...) |
CreateSP() |
TBaseSPMethodDelegateInstance<...>::Create(...) |
CreateThreadSafeSP() |
TBaseSPMethodDelegateInstance<...>::Create(...) |
CreateUFunction() |
TBaseUFunctionDelegateInstance<...>::Create(...) |
CreateUObject() |
TBaseUObjectMethodDelegateInstance<...>::Create(...) |
補充
這裡看起來沒有介紹帶參數、返回值的情況,因為實際上帶參數、返回值的也是調用了FUNC_DECLARE_DELEGATE
,調用的typedef
都一樣,只是傳入到模板的參數數量不一樣(藉助了C++11中的可變模板參數實現)。
最終還是:
另外,這裡的__VA_ARGS__
實際上就是:
比較容易理解,所以這裡不作詳細解釋。
2.1.1.f 總結
總而言之,單播委託的使用流程如下圖所示:
開始 –> 使用宏定義委託類型 –>聲明委託對象–>
綁定需要執行的函數指針到委託對象上–>|需要的時候|觸發委託對象並執行指針指向的函數;
觸發委託對象並執行指針指向的函數–>|不再需要綁定的函數|從委託對象中解綁函數–>|不再需要委託對象|銷毀委託對象;
從委託對象中解綁函數–>|綁定新的函數|綁定需要執行的函數指針到委託對象上;
觸發委託對象並執行指針指向的函數–>|指向的函數失效|報錯;
而委託的類層次結構我們可以總結為(其實我不是特別熟悉UML圖,希望沒有錯):
FDelegateBase <|– TDelegateBase
TDelegateBase <|– TDelegate
TDelegate ..|> FDefaultDelegateUserPolicy
TDelegateBase ..|> FDefaultDelegateUserPolicy
FDelegateBase *– IDelegateInstance
IDelegateInstance <|– IBaseDelegateInstance
IBaseDelegateInstance <|– TCommonDelegateInstanceState
TCommonDelegateInstanceState <|– TBaseStaticDelegateInstance
TCommonDelegateInstanceState <|– TBaseFunctorDelegateInstance
TCommonDelegateInstanceState <|– TWeakBaseFunctorDelegateInstance
TCommonDelegateInstanceState <|– TBaseRawMethodDelegateInstance
TCommonDelegateInstanceState <|– TBaseSPMethodDelegateInstance
TCommonDelegateInstanceState <|– TBaseUFunctionDelegateInstance
TCommonDelegateInstanceState <|– TBaseUObjectMethodDelegateInstance
TDelegate *– TBaseStaticDelegateInstance
TDelegate *– TBaseFunctorDelegateInstance
TDelegate *– TWeakBaseFunctorDelegateInstance
TDelegate *– TBaseRawMethodDelegateInstance
TDelegate *– TBaseSPMethodDelegateInstance
TDelegate *– TBaseUFunctionDelegateInstance
TDelegate *– TBaseUObjectMethodDelegateInstance
FDefaultDelegateUserPolicy : +FDelegateInstanceExtras(IDelegateInstance)
FDefaultDelegateUserPolicy : +FDelegateExtras(FDelegateBase)
FDefaultDelegateUserPolicy : +FMulticastDelegateExtras(TMulticastDelegateBase<fdefaultdelegateuserpolicy>)
class TDelegate {
+CreateStatic(…)
+CreateLambda(…)
+CreateWeakLambda(…)
+CreateRaw(…)
+CreateSP(…)
+CreateThreadSafeSP(…)
+CreateUFunction(…)
+CreateUObject(…)
+TDelegate(TDelegate&& Other)
+TDelegate& operator=(TDelegate&& Other)
+BindStatic(…)
+BindLambda(…)
+BindWeakLambda(…)
+BindRaw(…)
+BindSP(…)
+BindThreadSafeSP(…)
+BindUFunction(…)
+BindUObject(…)
}
class TDelegateBase {
-class TMulticastDelegateBase
-Super(FDelegateBase)
}
class FDelegateBase {
#FDelegateBase& operator=(FDelegateBase&& Other)
#void Unbind()
#IDelegateInstance* GetDelegateInstanceProtected() const
-void* Allocate(int32 Size)
-FDelegateAllocatorType::ForElementType<falignedinlinedelegatetype> DelegateAllocator
-int32 DelegateSize
}
class IDelegateInstance {
<<interface>>
+FName TryGetBoundFunctionName()
+UObject* GetUObject()
+const void* GetObjectForTimerManager() const
+uint64 GetBoundProgramCounterForTimerManager() const
+bool HasSameObject( const void* InUserObject ) const
+bool IsCompactable( )
+bool IsSafeToExecute( )
+FDelegateHandle GetHandle() const
}
class TCommonDelegateInstanceState {
#TTuple<vartypes…> Payload
#FDelegateHandle Handle
+RetValType(InRetValType)
}
此外:
- 單播委託支援PayLoad功能,但是動態類型的委託並不支援PayLoad;
- 單播委託在執行之前必須要
IsBound()
檢查是否已經綁定,否則會報錯; - 單播委託允許綁定函數帶有返回值;
其實單播委託理解了後面的都不難理解了,因此後面的內容會沒有單播委託這麼詳細(畢竟實現都相似的)。
2.1.2 動態(單播)委託
注意:這裡討論的是動態單播委託,動態多播委託後面會另外介紹
-
動態其實是指能夠被序列化,允許動態綁定,除此之外實際上和單播代理沒有太大區別;
-
動態委託也可以有返回值,但是只能有一個返回值;
-
動態即能夠被序列化,因此可以在藍圖中使用,也可以添加
UPROPERTY
; -
綁定的時候不需要函數指針,只需要函數名稱字元串,但是只能夠綁定
UFUNCTION
; -
動態委託運行是最慢的,所以如果沒有必要的話就用別的委託;
2.1.2.a 聲明
其實和單播委託的聲明差不多:
DECLARE_DYNAMIC_DELEGATE[_RetVal, ...]( DelegateName );
// 例如無參數,無返回值
DECLARE_DYNAMIC_DELEGATE(FNoRetNoParamDelegate);
// 例如1個參數無返回值,最多支援到9個,注意和前面不同,給定參數類型的同時要加上類型名字,並且綁定參數和委託要保持一致
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnAssetLoaded, class UObject*, Loaded);
// 例如1個返回值一個參數
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(UWidget*, FGenerateWidgetForObject, UObject*, Item);
2.1.2.b 綁定
4.26的動態委託綁定函數只有一個BindUFunction
,並提供UObject
對象、函數名字即可。
2.1.2.c 執行委託
和單播委託類似:
2.1.2.d 底層實現
可以看到實際上依託TBaseDynamicDelegate
來實現,而且宏定義聲明一個動態委託就是聲明了一個類繼承TBaseDynamicDelegate
。宏定義裡面也另外定義了ExecuteIfBound
與Execute
函數,實際執行委託也是通過宏定義裡面定義的這兩個函數,同時依託UE4的反射、序列化機制實現的。
TBaseDynamicDelegate
內的實現不多,實際上還是得依靠TScriptDelegate
:
TScriptDelegate
才是真正保存函數名字、綁定的對象的弱指針的地方:
簡單看下綁定部分,因為只能綁定UFUNCTION
函數,所以只有一個綁定函數:
執行則是依託一開始宏定義裡面定義的Execute(傳入參數)
:
實際執行的時候UE4會根據輸入的函數名字找到對應的函數並執行,這個函數最終會被上面定義的Execute
調用:
2.1.2.e 總結
因為比較簡單,所以這裡就先不花UML圖來解析了。
- 實際上聲明一個動態委託類型就是創建了一個繼承
TBaseDynamicDelegate
的類,並且類名為動態委託的名字; - 動態委託在執行時需要實時在類中按照給定的函數名字查找對應的函數,因此執行速度很慢,所以如果能用別的不是動態的委託代替就用別的委託[11];
- 動態委託能夠被藍圖調用;
- 動態委託能夠序列化,因此可以被存儲到硬碟上;
- 綁定的時候不需要函數指針,只需要函數名稱字元串,但是只能夠綁定
UFUNCTION
;
2.1.3 多播委託
吐個槽,官方文檔真的是一言難盡,只是multicast delegate這個詞在中文頁面上都有2種不同的翻譯。更加關鍵的是,多播委託的官方文檔居然還有低級錯誤,在《多播委託》頁面最上面寫明了「多播委託不能使用返回值」,下面給的聲明多播委託示例就帶了個返回值。
- 多播委託能綁定多個函數指針,委託被執行的時候也會觸發多個函數;
- 多播委託執行的時候,執行綁定該委託的函數的順序實際上是沒有規定的(因此可能最後綁定的函數最先被執行)
- 多播委託不允許有返回值。實際上底層是一個保存了所有綁定了這個委託的函數的
FDelegateBase
數組,執行委託的時候會遍曆數組並調用綁定的函數
2.1.3.a 聲明
DECLARE_MULTICAST_DELEGATE<參數數量>( DelegateName, ParamsTypes );
// 例如0個參數
DECLARE_MULTICAST_DELEGATE( DelegateName );
// 例如1個參數
DECLARE_MULTICAST_DELEGATE_OneParam( DelegateName, Param1Type );
比較簡單,和前面的委託都差不多。
2.1.3.b 綁定
先以綁定UObject
對象的成員函數為例:
UDelegatepTestClass* UObjMC = NewObject<udelegateptestclass>(this, UDelegatepTestClass::StaticClass());
// 先傳入UObject,然後傳入成員函數指針
CharacterDelegateMulticast7.AddUObject(UObjMC, &UDelegatepTestClass::DelegateProc1);
其他的綁定方式差不太多,故在此不贅述。
所有的綁定方式如下:
函數名 | 用途 |
---|---|
FDelegateHandle Add(const FDelegate& InNewDelegate) |
將某個函數委託添加到該多播委託的調用列表中 |
FDelegateHandle AddStatic(...) |
添加原始C++指針全局函數委託 |
FDelegateHandle AddLambda(...) |
添加匿名函數委託 |
FDelegateHandle AddWeakLambda(...) |
添加弱引用對象的匿名函數委託,會對對象弱引用 |
FDelegateHandle AddRaw(...) |
添加原始C++指針委託。原始指針不使用任何類型的引用,因此如果從委託下面刪除了對象,則調用此函數可能不安全。調用Execute()時請小心! |
FDelegateHandle AddSP(...) |
添加基於共享指針的(快速、非執行緒安全)成員函數委託,共享指針委託保留對對象的弱引用 |
FDelegateHandle AddThreadSafeSP(...) |
添加基於共享指針的成員函數委託(相對較慢,但是執行緒安全),會對對象弱引用 |
FDelegateHandle AddUFunction(...) |
添加UFunction 類型的成員函數,會對輸入的對象弱引用 |
FDelegateHandle AddUObject(...) |
添加UObject 對象的成員函數,會對輸入的對象弱引用 |
2.1.3.c 執行
委託.Broadcast()
即可,即便在沒有任何綁定的時候都可以用這個函數來觸發委託執行。不過需要注意的是,綁定函數的執行順序是未定義的,執行順序很可能與綁定順序不同(畢竟多播委託可能會多次添加、移除委託)。
2.1.3.d 底層實現
保存的綁定函數數組
先看宏定義:
接著往下看:
可以看到實際上是TMulticastDelegate
,看看它的定義:
和單播委託一樣,通過偏特化的方式保證UserPolicy
在有默認值的同時能夠讓用戶輸入自己定義的UserPolicy
。也是和單播委託一樣,實際上保存指針的數組並不在TMulticastDelegate
中,要在基類中查找,我們先看上一級的UserPolicy::FMulticastDelegateExtras
,即TMulticastDelegateBase<fdefaultdelegateuserpolicy>
:
可以看到,實際上就是一個TDelegateBase
數組(FMulticastInvocationListAllocatorType
先不用管,主要是和記憶體分配有關,與我們關注的重點不太相關)。
其實說到這裡基本上可以和單播委託那邊的分析結合起來看,但是首先,我們先接著看綁定的實現。
綁定的實現
首先我們看看常用的AddUObject
是怎麼實現的:
template <typename userclass,="" typename...="" vartypes="">
inline FDelegateHandle AddUObject(UserClass* InUserObject, typename TMemFunPtrType<false, userclass,="" void="" (paramtypes...,="" vartypes...)="">::Type InFunc, VarTypes... Vars)
{
static_assert(!TIsConst<userclass>::Value, "Attempting to bind a delegate with a const object pointer and non-const member function.");
// 這裡實際上調用了上面提到的FDelegate::CreateUObject,不理解的話看上面的內容即可
return Add(FDelegate::CreateUObject(InUserObject, InFunc, Vars...));
}
可以看到實際上還是依靠了另一個函數Add
,並且實際上用到了上面提到的單播委託的FDelegate::CreateUObject
來創建一個委託對象。那麼我們接著看看Add
的實現:
/**
* Adds a delegate instance to this multicast delegate's invocation list.
*
* @param Delegate The delegate to add.
*/
FDelegateHandle Add(FDelegate&& InNewDelegate)
{
FDelegateHandle Result;
if (Super::GetDelegateInstanceProtectedHelper(InNewDelegate))
{
Result = Super::AddDelegateInstance(MoveTemp(InNewDelegate));
}
return Result;
}
這裡的Super
其實是:
即TMulticastDelegateBase<fdefaultdelegateuserpolicy>
,因此最終會調用TMulticastDelegateBase
的:
而InvocationList
的定義是:
即用到上面定義的類型:
所以實際上就是先創建一個單播委託,然後添加到了自己維護的TArray
數組中。
執行委託的實現
/**
* Broadcasts this delegate to all bound objects, except to those that may have expired.
*
* The constness of this method is a lie, but it allows for broadcasting from const functions.
*/
void Broadcast(ParamTypes... Params) const
{
bool NeedsCompaction = false;
Super::LockInvocationList();
{
const InvocationListType& LocalInvocationList = Super::GetInvocationList();
// call bound functions in reverse order, so we ignore any instances that may be added by callees
for (int32 InvocationListIndex = LocalInvocationList.Num() - 1; InvocationListIndex >= 0; --InvocationListIndex)
{
// this down-cast is OK! allows for managing invocation list in the base class without requiring virtual functions
const FDelegate& DelegateBase = (const FDelegate&)LocalInvocationList[InvocationListIndex];
IDelegateInstance* DelegateInstanceInterface = Super::GetDelegateInstanceProtectedHelper(DelegateBase);
if (DelegateInstanceInterface == nullptr || !((DelegateInstanceInterfaceType*)DelegateInstanceInterface)->ExecuteIfSafe(Params...))
{
NeedsCompaction = true;
}
}
}
Super::UnlockInvocationList();
if (NeedsCompaction)
{
const_cast<tmulticastdelegate*>(this)->CompactInvocationList();
}
}
可以看到實際上就是遍歷一遍數組然後一個個調用ExecuteIfSafe(傳入參數)
。注意ExecuteIfSafe
,如果委託無法被執行,那麼就會返回false
。
而ExecuteIfSafe
的實現隨著不同類型的綁定函數而不同,例如如果綁定的是全局函數,實際上實現是:
bool ExecuteIfSafe(ParamTypes... Params) const final
{
// Call the static function
checkSlow(StaticFuncPtr != nullptr);
(void)this->Payload.ApplyAfter(StaticFuncPtr, Params...);
return true;
}
可以看到無論如何都會執行,但是如果是別的,例如綁定的是weaklambda
,那麼:
bool ExecuteIfSafe(ParamTypes... Params) const final
{
if (ContextObject.IsValid())
{
(void)this->Payload.ApplyAfter(Functor, Params...);
return true;
}
return false;
}
會判斷弱引用的對象是不是還有效,如果已經被銷毀了就不會執行並且返回false
。
這樣就可以保證無論何時調用Broadcast()
都是安全的。
2.1.3.e 總結
-
實際上多播委託就是維護了一個由單播委託組成的數組,依託單播委託實現的;
-
無論何時調用
Broadcast()
都是安全的。
2.1.4 事件
事件和多播委託相似(實際上就是多播,只是多了個friend class OwningType
,用來辨別調用者是不是代理擁有者),功能都差不多,只是限定死了部分函數的許可權:只有聲明事件的類可以調用事件的Broadcast
、IsBound
和Clear
函數。這就保證了只有事件的擁有者能夠觸發事件。
事件綁定的函數也是不能夠有返回值的。
// 和組播類似
// 注意首個參數,用來指定事件擁有者
DECLARE_EVENT( OwningType, EventName );
// 1個參數
DECLARE_EVENT_OneParam( OwningType, EventName, Param1Type );
// 2個參數
DECLARE_EVENT_TwoParams( OwningType, EventName, Param1Type, Param2Type );
// 多個參數
DECLARE_EVENT_<num>Params( OwningType, EventName, Param1Type, Param2Type, ...);
事件和多播基本一致,而且因為後面的版本中事件類型會被移除,因此這裡不進行詳細說明。
2.1.5 動態多播委託
實際上上面已經詳細說明了動態委託、多播委託,如果上面的內容理解了的話那麼這裡的內容也是很容易能夠理解的了。
2.1.5.a 聲明
// 動態多播不能有返回值,所以只列舉有參數、無參數的例子
// 無參數
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOpenViewDelegate_DynamicMulticast);
// 1個參數,和前面不同的是要加上參數名字
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCharacterDelegate_DynamicMulticast, int, nCode);
2.1.5.b 綁定
綁定函數 | 使用場景 |
---|---|
Add |
添加一個函數委託 |
AddUnique |
添加一個函數委託,但是只有在這個函數委託不存在維護的數組中的時候才添加(根據委託的簽名是否已經存在數組中進行判斷) |
AddDynamic |
用來綁定一個UObject 類型的成員函數到委託中(這個介面實際上通過宏重定向到__Internal_AddDynamic ) |
AddUniqueDynamic |
與上面的AddDynamic 一樣,但是會根據函數的簽名確保不重複添加 |
2.1.5.c 執行
直接調用Broadcast(輸入參數)
即可,任何時候都可以調用這個函數,與多播委託一樣。
2.1.5.d 底層實現
/** Declares a blueprint-accessible broadcast delegate that can bind to multiple native UFUNCTIONs simultaneously */
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE( DelegateName ) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_DELEGATE) FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE( FWeakObjectPtr, DelegateName, DelegateName##_DelegateWrapper, , FUNC_CONCAT( *this ), void )
可以看出實際上是調用了FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE
:
/** Declare user's dynamic multi-cast delegate, with wrapper proxy method for executing the delegate */
#define FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE(TWeakPtr, DynamicMulticastDelegateClassName, ExecFunction, FuncParamList, FuncParamPassThru, ...) \
class DynamicMulticastDelegateClassName : public TBaseDynamicMulticastDelegate<tweakptr, __va_args__=""> \
{ \
public: \
/** Default constructor */ \
DynamicMulticastDelegateClassName() \
{ \
} \
\
/** Construction from an FMulticastScriptDelegate must be explicit. This is really only used by UObject system internals. */ \
explicit DynamicMulticastDelegateClassName( const TMulticastScriptDelegate<>& InMulticastScriptDelegate ) \
: TBaseDynamicMulticastDelegate<tweakptr, __va_args__="">( InMulticastScriptDelegate ) \
{ \
} \
\
/** Broadcasts this delegate to all bound objects, except to those that may have expired */ \
void Broadcast( FuncParamList ) const \
{ \
ExecFunction( FuncParamPassThru ); \
} \
};
可以看到,實際上和動態委託類似,會變成一個繼承TBaseDynamicMulticastDelegate
的類:
TBaseDynamicMulticastDelegate
提供了__Internal_AddDynamic
的實現:
/**
* Binds a UObject instance and a UObject method address to this multi-cast delegate.
*
* @param InUserObject UObject instance
* @param InMethodPtr Member function address pointer
* @param InFunctionName Name of member function, without class name
*
* NOTE: Do not call this function directly. Instead, call AddDynamic() which is a macro proxy function that
* automatically sets the function name string for the caller.
*/
template< class UserClass >
void __Internal_AddDynamic( UserClass* InUserObject, typename FDelegate::template TMethodPtrResolver< UserClass >::FMethodPtr InMethodPtr, FName InFunctionName )
{
check( InUserObject != nullptr && InMethodPtr != nullptr );
// NOTE: We're not actually storing the incoming method pointer or calling it. We simply require it for type-safety reasons.
FDelegate NewDelegate;
NewDelegate.__Internal_BindDynamic( InUserObject, InMethodPtr, InFunctionName );
this->Add( NewDelegate );
}
最終調用的Add
則由基類TMulticastScriptDelegate
實現:
而且最終保存的數組實際上也保存在TMulticastScriptDelegate
中:
可以看到,實際上就是一個數組,裡面保存了一系列的動態委託。而Broadcast(傳入參數)
最終會調用到TMulticastScriptDelegate
的:
/**
* Executes a multi-cast delegate by calling all functions on objects bound to the delegate. Always
* safe to call, even if when no objects are bound, or if objects have expired. In general, you should
* never call this function directly. Instead, call Broadcast() on a derived class.
*
* @param Params Parameter structure
*/
template <class uobjecttemplate="">
void ProcessMulticastDelegate(void* Parameters) const
{
if( InvocationList.Num() > 0 )
{
// Create a copy of the invocation list, just in case the list is modified by one of the callbacks during the broadcast
typedef TArray< TScriptDelegate<tweakptr>, TInlineAllocator< 4 > > FInlineInvocationList;
FInlineInvocationList InvocationListCopy = FInlineInvocationList(InvocationList);
// Invoke each bound function
for( typename FInlineInvocationList::TConstIterator FunctionIt( InvocationListCopy ); FunctionIt; ++FunctionIt )
{
if( FunctionIt->IsBound() )
{
// Invoke this delegate!
FunctionIt->template ProcessDelegate<uobjecttemplate>(Parameters);
}
else if ( FunctionIt->IsCompactable() )
{
// Function couldn't be executed, so remove it. Note that because the original list could have been modified by one of the callbacks, we have to search for the function to remove here.
RemoveInternal( *FunctionIt );
}
}
}
}
與多播委託類似,也是會在調用前先用FunctionIt->IsBound()
進行判斷,確保執行安全。當然,前面提到了動態委託運行速度很慢,所以您可以猜到動態多播會是本文中所有的委託中執行最慢的。
參考
注意:因為文章經過多次修改,因此實際上這裡的順序與文中提及的順序不一致。LaTeX對引用順序的處理就很好,所以後面我可能會考慮改用LaTeX來做這類筆記
-
UE4-深入委託Delegate實現原理:這篇文章可以說是幫了大忙,不過本文部分內容實際上參考了這裡的分析。但是文中有一部分內容已經對應不上4.26及以後的版本的源程式碼了。不過,還是非常值得一看,強烈推薦
</tweakptr,></tweakptr,></tmulticastdelegate*></false,></vartypes…></functype,></typename…></functype,></retvaltype(paramtypes…),></retvaltype(paramtypes…),></functype,></retvaltype(paramtypes…),></retvaltype(paramtypes…),></typename…></inretvaltype(paramtypes…),></returntype(一堆您傳入的參數)> ↩︎ -
FFuncPtr官方文檔:這個官方文檔和往常一樣,寫得和沒寫一樣,建議看別的 ↩︎