0%

探究多播机制

前言

在前面一篇中,我们认识了delegate,具体内容可以看上一篇。

前两天遇到一个crash,就是提示我 Slate can only be accessed from the GameThread or the SlateLoadingThread!

具体操作是—— 我绑定了event到delegate,在event里我会对UI进行操作,结果就是crash报上面的错误。

根据提示信息,在broadcast()的位置调用IsGameThread()发现当前线程不是GameThread,而UE要求对UI的操作必须在GameThread线程,否则会直接crash。(从这里也能看出来,UE引擎是很严密的,正是这个严密,才给了我们很大的自由度)。

解决思路就是把UI操作放到GameThread里,我用的是AsyncTask方法,这个大家上网查一下就可以了,这里不再赘述。我想谈论的是delegate在broadcast() 或者 ExecuteIfBound() 的时候,到底是一个什么样的流程,是在哪个线程中执行的?是异步执行吗?多broadcast()来说,多个绑定的event执行有先后顺序吗?

DELEGATE宏定义及其展开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
#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 )

#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 ); \
} \
};


/**
* Script multi-cast delegate base class
*/
template <typename TWeakPtr = FWeakObjectPtr>
class TMulticastScriptDelegate
{
public:

/**
* Default constructor
*/
inline TMulticastScriptDelegate() { }

public:

/**
* Checks to see if any functions are bound to this multi-cast delegate
*
* @return True if any functions are bound
*/
inline bool IsBound() const
{
UE_DELEGATES_MT_SCOPED_READ_ACCESS(AccessDetector);

return InvocationList.Num() > 0;
}

/**
* Checks whether a function delegate is already a member of this multi-cast delegate's invocation list
*
* @param InDelegate Delegate to check
* @return True if the delegate is already in the list.
*/
bool Contains( const TScriptDelegate<TWeakPtr>& InDelegate ) const
{
UE_DELEGATES_MT_SCOPED_READ_ACCESS(AccessDetector);

return InvocationList.Contains( InDelegate );
}

/**
* Checks whether a function delegate is already a member of this multi-cast delegate's invocation list
*
* @param InObject Object of the delegate to check
* @param InFunctionName Function name of the delegate to check
* @return True if the delegate is already in the list.
*/
bool Contains( const UObject* InObject, FName InFunctionName ) const
{
UE_DELEGATES_MT_SCOPED_READ_ACCESS(AccessDetector);

return InvocationList.ContainsByPredicate( [=]( const TScriptDelegate<TWeakPtr>& Delegate ){
return Delegate.GetFunctionName() == InFunctionName && Delegate.IsBoundToObjectEvenIfUnreachable(InObject);
} );
}

/**
* Adds a function delegate to this multi-cast delegate's invocation list
*
* @param InDelegate Delegate to add
*/
void Add( const TScriptDelegate<TWeakPtr>& InDelegate )
{
UE_DELEGATES_MT_SCOPED_WRITE_ACCESS(AccessDetector);

// First check for any objects that may have expired
CompactInvocationList();

// Add the delegate
AddInternal( InDelegate );
}

/**
* Adds a function delegate to this multi-cast delegate's invocation list if a delegate with the same signature
* doesn't already exist in the invocation list
*
* @param InDelegate Delegate to add
*/
void AddUnique( const TScriptDelegate<TWeakPtr>& InDelegate )
{
UE_DELEGATES_MT_SCOPED_WRITE_ACCESS(AccessDetector);

// Add the delegate, if possible
AddUniqueInternal( InDelegate );

// Then check for any objects that may have expired
CompactInvocationList();
}

/**
* Removes a function from this multi-cast delegate's invocation list (performance is O(N)). Note that the
* order of the delegates may not be preserved!
*
* @param InDelegate Delegate to remove
*/
void Remove( const TScriptDelegate<TWeakPtr>& InDelegate )
{
UE_DELEGATES_MT_SCOPED_WRITE_ACCESS(AccessDetector);

// Remove the delegate
RemoveInternal( InDelegate );

// Check for any delegates that may have expired
CompactInvocationList();
}

/**
* Removes a function from this multi-cast delegate's invocation list (performance is O(N)). Note that the
* order of the delegates may not be preserved!
*
* @param InObject Object of the delegate to remove
* @param InFunctionName Function name of the delegate to remove
*/
void Remove( const UObject* InObject, FName InFunctionName )
{
UE_DELEGATES_MT_SCOPED_WRITE_ACCESS(AccessDetector);

// Remove the delegate
RemoveInternal( InObject, InFunctionName );

// Check for any delegates that may have expired
CompactInvocationList();
}

/**
* Removes all delegate bindings from this multicast delegate's
* invocation list that are bound to the specified object.
*
* This method also compacts the invocation list.
*
* @param InObject The object to remove bindings for.
*/
void RemoveAll(const UObject* Object)
{
UE_DELEGATES_MT_SCOPED_WRITE_ACCESS(AccessDetector);

for (int32 BindingIndex = InvocationList.Num() - 1; BindingIndex >= 0; --BindingIndex)
{
const TScriptDelegate<TWeakPtr>& Binding = InvocationList[BindingIndex];

if (Binding.IsBoundToObject(Object) || Binding.IsCompactable())
{
InvocationList.RemoveAtSwap(BindingIndex);
}
}
}

/**
* Removes all functions from this delegate's invocation list
*/
void Clear()
{
UE_DELEGATES_MT_SCOPED_WRITE_ACCESS(AccessDetector);

InvocationList.Empty();
}

/**
* Converts this delegate to a string representation
*
* @return Delegate in string format
*/
template <typename UObjectTemplate>
inline FString ToString() const
{
UE_DELEGATES_MT_SCOPED_READ_ACCESS(AccessDetector);

if( IsBound() )
{
FString AllDelegatesString = TEXT( "[" );
bool bAddComma = false;
for( typename FInvocationList::TConstIterator CurDelegate( InvocationList ); CurDelegate; ++CurDelegate )
{
if (bAddComma)
{
AllDelegatesString += TEXT( ", " );
}
bAddComma = true;
AllDelegatesString += CurDelegate->template ToString<UObjectTemplate>();
}
AllDelegatesString += TEXT( "]" );
return AllDelegatesString;
}
return TEXT( "<Unbound>" );
}

/** Multi-cast delegate serialization */
friend FArchive& operator<<( FArchive& Ar, TMulticastScriptDelegate<TWeakPtr>& D )
{
UE_DELEGATES_MT_SCOPED_WRITE_ACCESS(D.AccessDetector);

if( Ar.IsSaving() )
{
// When saving the delegate, clean up the list to make sure there are no bad object references
D.CompactInvocationList();
}

Ar << D.InvocationList;

if( Ar.IsLoading() )
{
// After loading the delegate, clean up the list to make sure there are no bad object references
D.CompactInvocationList();
}

return Ar;
}

friend void operator<<(FStructuredArchive::FSlot Slot, TMulticastScriptDelegate<TWeakPtr>& D)
{
UE_DELEGATES_MT_SCOPED_WRITE_ACCESS(D.AccessDetector);

FArchive& UnderlyingArchive = Slot.GetUnderlyingArchive();

if (UnderlyingArchive.IsSaving())
{
// When saving the delegate, clean up the list to make sure there are no bad object references
D.CompactInvocationList();
}

Slot << D.InvocationList;

if (UnderlyingArchive.IsLoading())
{
// After loading the delegate, clean up the list to make sure there are no bad object references
D.CompactInvocationList();
}
}

/**
* 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
{
UE_DELEGATES_MT_SCOPED_WRITE_ACCESS(AccessDetector);

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 );
}
}
}
}

从宏定义上看,我们声明一个DELEGATE的时候,实际上是声明了一个类,这个类向上的父类传递依次是 TBaseDynamicMulticastDelegate 、TMulticastScriptDelegate。

TMulticastScriptDelegate类里的接口包括绑定接口 Add 、AddUnique,实际执行的时候,我们看到broadcast接口在TBaseDynamicMulticastDelegate 里,从宏定义上看是直接执行了

1
2
3
4
voidBroadcast( FuncParamList )const
{
ExecFunction( FuncParamPassThru );
}

那这里的ExecFunction是绑定好的event or function?FuncParamPassThru是传递的参数?

先说结论,broadcast是阻塞同步操作,所有bounded event不是并行执行,是根据存在InvocationList 中的顺序执行,看 ProcessMulticastDelegate() 接口的注释就能明白了。

但还是有些糊涂,broadcast是怎么跟 ProcessMulticastDelegate 联系起来的呢。

我们把宏定义展开就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE( DelegateName ) \
CURRENT_FILE_ID##_##__LINE__##_DELEGATE \
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 ); \
} \
|

这里我们发现几个问题

CURRENT_FILE_ID##_##LINE##_DELEGATE 在类声明开头有什么用途?
FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE(TWeakPtr, DynamicMulticastDelegateClassName, ExecFunction, FuncParamList, FuncParamPassThru, …)里 ExecFuction FunctionParamlist FuncParamPassThru,我似乎之后到这几个参数是什么意思,但它们从何而来?展开之后编译难道不会找不到定义而报错吗?
这个问题要继续探究一下,所以本文后面会再做修订。

回来了,继续更新。

从网上找了些资料,发现一个工具 UHT(Unreal Header Tool)用来扫描 .h文件里的UFUNCTION UCLASS以及其它的一些宏定义,来生成相应的代码。关于UHT的作用,估计得新开一章,这里暂时就知道UHT会生成我们上面所提到的宏就行了。

下面我们举例子来说。

新建一个动态多播代理

1
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FTestDelegate);

用UHT工具会生成如下代码

1
2
3
4
5
6
7
8
#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID FID_AnimationSample_Source_AnimationSample_AnimationSampleCharacter_h

#define FID_AnimationSample_Source_AnimationSample_AnimationSampleCharacter_h_9_DELEGATE \
static inline void FTestDelegate_DelegateWrapper(const FMulticastScriptDelegate& TestDelegate) \
{ \
TestDelegate.ProcessMulticastDelegate<UObject>(NULL); \
}

这里 BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,LINE,_DELEGATE) 的定义就是 FID_AnimationSample_Source_AnimationSample_AnimationSampleCharacter_h_9_DELEGATE,本质上就是一个处理绑定函数的静态方法。

这样上面我们感觉缺失的参数 DelegateName##_DelegateWrapper 在这里就找到定义了。

这样下来,我们的宏全部展开后就成了下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#define FUNC_CONCAT( ... ) __VA_ARGS__

#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID FID_AnimationSample_Source_AnimationSample_AnimationSampleCharacter_h

#define FID_AnimationSample_Source_AnimationSample_AnimationSampleCharacter_h_9_DELEGATE \
static inline void FTestDelegate_DelegateWrapper(const FMulticastScriptDelegate& TestDelegate) \
{ \
TestDelegate.ProcessMulticastDelegate<UObject>(NULL); \
}
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE( FTestDelegate ) \
static inline void FTestDelegate_DelegateWrapper(const FMulticastScriptDelegate& TestDelegate) \
{ \
TestDelegate.ProcessMulticastDelegate<UObject>(NULL); \
} \

class FTestDelegate: public TBaseDynamicMulticastDelegate<FWeakObjectPtr, __VA_ARGS__> \
{ \
public: \
/** Default constructor */ \
FTestDelegate() \
{ \
} \
\
/** Construction from an FMulticastScriptDelegate must be explicit. This is really only used by UObject system internals. */ \
explicit FTestDelegate( 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 \
{ \
FTestDelegate_DelegateWrapper( FUNC_CONCAT( *this ) ); \
} \
};

__VA_ARGS__ 是C/C++宏中的特殊标识符,用于表示宏参数的可变参数列表,此时 __VA_ARGS__ 所代表的参数只有 *this,而 #define FUNC_CONCAT( … ) __VA_ARGS__ 的意思是 FUNC_CONCAT 宏 是将 宏参数 展开为逗号分隔的列表,所以此时再进一步替换为如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#define FUNC_CONCAT( ... ) __VA_ARGS__

#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID FID_AnimationSample_Source_AnimationSample_AnimationSampleCharacter_h

#define FID_AnimationSample_Source_AnimationSample_AnimationSampleCharacter_h_9_DELEGATE \
static inline void FTestDelegate_DelegateWrapper(const FMulticastScriptDelegate& TestDelegate) \
{ \
TestDelegate.ProcessMulticastDelegate<UObject>(NULL); \
}
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE( FTestDelegate ) \
static inline void FTestDelegate_DelegateWrapper(const FMulticastScriptDelegate& TestDelegate) \
{ \
TestDelegate.ProcessMulticastDelegate<UObject>(NULL); \
} \

class FTestDelegate: public TBaseDynamicMulticastDelegate<FWeakObjectPtr, *this> \
{ \
public: \
/** Default constructor */ \
FTestDelegate() \
{ \
} \
\
/** Construction from an FMulticastScriptDelegate must be explicit. This is really only used by UObject system internals. */ \
explicit FTestDelegate( const TMulticastScriptDelegate<>& InMulticastScriptDelegate ) \
: TBaseDynamicMulticastDelegate<TWeakPtr, __VA_ARGS__>( InMulticastScriptDelegate ) \
{ \
} \
\
/** Broadcasts this delegate to all bound objects, except to those that may have expired */ \
void Broadcast( ) const \
{ \
FTestDelegate_DelegateWrapper( *this ); \
} \
};

到这里为止,从代码层面看,完全都说得通了。

但还有几个遗留问题

FuncParamList 和 FuncParamPassThru 有什么区别?
对UHT更深入地理解
其它未想到的问题
后面希望能出一篇关于UHT的文章,结合UHT源码,剖析架构。

其它问题就等想起来再说了。