Unity 광고 SDK 콜백 크래시 - 메인 스레드 디스패처와 마샬링
이 글은 제 개인적인 공부를 위해 작성한 글입니다. 틀린 내용이 있을 수 있고, 피드백은 환영합니다.
문제
Unity Ads 혹은 Google Admob로 광고를 보고, 완료 혹은 실패 시에 콜백에서 유니티 컴포넌트(ugui 같은)에 접근하면 크래시가 터진다.
아래 예시처럼 꼭 FindObjectsOfType 뿐만 아니라 AddComponent 같은 다른 유니티 API를 호출하는 경우에도 동일하게 크래시가 발생한다.
1
UnityException FindObjectsOfType can only be called from the main thread.
원인
GameObject, Transform, Component 등 네이티브 엔진의 씬 그래프에 접근하는 API들은 메인 스레드에서만 접근할 수 있는데, sdk 콜백에서 이런 API를 호출하도록 코드를 짜고, sdk 콜백이 자체 스레드(워커 스레드)에서 실행되는 경우가 있어 터지는거다.
해결
이걸 해결할 수 있는 몇 가지 방법이 있는데, 첫 번째로 Google Admob의 경우 RaiseAdEventsOnUnityMainThread 플래그를 활성화하는 거다.
Google Admob 8.0.0부터 추가된 플래그인데, change log를 보면 이렇게 써있다.
1
- Added the MobileAds.RaiseAdEventsOnUnityMainThread option for raising ad events on the Unity main thread.
하지만 Unity Ads는 이러한 기능을 지원하는지는 좀 더 찾아봐야 될 거 같고, 어짜피 다른 sdk를 사용할 때 이놈들도 메인 스레드에서 콜백을 받을 수 있다는 보장은 없다.
그렇다면 더 확실하고, 범용적으로 사용할 수 있는 방법으로는 메인 스레드 디스패처를 사용하는거다. 직접 만들어도 좋지만, UnityMainThreadDispatcher에 잘 만들어진 디스패처도 있고 UniRX에도 MainThreadDispatcher를 지원한다.
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
// UniRX의 MainThreadDispatcher.cs
public sealed class MainThreadDispatcher : MonoBehaviour
{
// ...
ThreadSafeQueueWorker queueWorker = new ThreadSafeQueueWorker();
static MainThreadDispatcher instance;
static MainThreadDispatcher Instance
{
get
{
Initialize();
return instance;
}
}
// ...
public static void Initialize()
{
if (!initialized)
{
MainThreadDispatcher dispatcher = null;
try
{
dispatcher = GameObject.FindObjectOfType<MainThreadDispatcher>();
}
catch
{
// Throw exception when calling from a worker thread.
var ex = new Exception("UniRx requires a MainThreadDispatcher component created on the main thread. Make sure it is added to the scene before calling UniRx from a worker thread.");
UnityEngine.Debug.LogException(ex);
throw ex;
}
if (isQuitting)
{
// don't create new instance after quitting
// avoid "Some objects were not cleaned up when closing the scene find target" error.
return;
}
if (dispatcher == null)
{
// awake call immediately from UnityEngine
new GameObject("MainThreadDispatcher").AddComponent<MainThreadDispatcher>();
}
else
{
dispatcher.Awake(); // force awake
}
}
}
/// <summary>Dispatch Asyncrhonous action.</summary>
public static void Post(Action<object> action, object state)
{
var dispatcher = Instance;
if (!isQuitting && !object.ReferenceEquals(dispatcher, null))
{
dispatcher.queueWorker.Enqueue(action, state);
}
}
void Update()
{
if (update != null)
{
try
{
update.OnNext(Unit.Default);
}
catch (Exception ex)
{
unhandledExceptionCallback(ex);
}
}
queueWorker.ExecuteAll(unhandledExceptionCallback);
}
// ...
}
스크립트 전체도 700줄 정도 밖에 안되지만 눈여겨 볼만한 코드만 가져왔는데, Post 메서드로 메인 스레드에서 실행하고 싶은 액션을 큐에 등록하고, Update에서 등록된 액션들을 실행하는 구조다. 하고자 하는 행동을 큐에 쌓아두기에 콜백이 덮어씌워지거나 유실되지 않는다.
큐가 UniRX에 따로 구현되어 있는 ThreadSafeQueueWorker를 사용하는데, lock을 통해 thread-safe한 건 물론이고 2개의 대기열 큐를 가지고 있어 유니티 메인 스레드에서 일을 처리하는 동안, 다른 스레드들이 새 일을 큐에 삽입할 수 있다.
그리고 Initialize 메서드도 눈여겨 봐야 된다. 우리가 이 메인 스레드 디스패처를 사용하는 이유를 다시 생각해보면 메인 스레드에서만 사용할 수 있는 유니티 API를 워커 스레드에서 접근하였기 때문이다. 이 MainThreadDispatcher 또한 MonoBehaviour를 상속받고 싱글톤 성격을 띄고 있으며, 외부에서 따로 초기화 함수를 호출하지 않는 이상 늦은 초기화를 하게 되어 있다.
만약에 처음으로 이 MainThreadDispatcher를 호출한 곳이 워커 스레드라면(admob 콜백 같은) 초기화 과정에서 GameObject.FindObjectOfType<MainThreadDispatcher>()를 호출하게 되어 똑같은 크래시가 나게 된다. 크래시를 막기 위해 디스패처를 사용했지만 디스패처가 크래시를 내는 아이러니한 상황이 나오게 되는거다;;
운이 좋게 프로젝트 내에서 UniRX를 다른 곳에서도 쓰고 있어서, 다른 코드가 먼저 메인 스레드에서 UniRX를 건드려 “우연히” 초기화가 되었다 하면 더 큰 문제이다. 초기화 순서에 대한 우연이기에 재현하기도 어려운 간헐적인 버그를 만들게 된다.
여러 번 초기화 함수가 호출되더라도 분기문에서 알아서 막히니 명시적으로 게임 시작 지점에서 디스패처를 초기화해주는 것이 좋다.
정리하자면,
- GameObject, Transform, Component 같은 네이티브 엔진 API들은 메인 스레드에서만 접근할 수 있다.
- sdk의 콜백이 워커 스레드에서 실행된다면, 메인 스레드 디스패처 패턴으로 우회하자.
- UniRX의 MainThreadDispatcher를 사용한다면 초기화 함수를 게임 시작 시에 명시적으로 호출해주자.
이걸 스레드 마샬링(thread marshalling)이라 부르는데, 광고 콜백(워커 스레드)에서 만든 실행 코드를 메인 스레드로 넘기는 것이다. 스레드 마샬링은 실행 컨텍스트를 다른 스레드로 옮기는 것을 의미한다. “메인 스레드로 디스패치한다”는 것과 “메인 스레드로 마샬링한다”는 것은 같은 말이다.
참고