SetActive() 只能从主线程调用
Posted
技术标签:
【中文标题】SetActive() 只能从主线程调用【英文标题】:SetActive() can only be called from the main thread 【发布时间】:2018-12-24 18:06:39 【问题描述】:我被这个问题困扰了 3 天,我做了很多研究,但找不到任何答案,这里是正在发生的事情的简要说明,尝试使用 Firebase 数据库和使用 Unity3D 进行身份验证,这里步骤是:
第一个用户登录,如果成功,它将从数据库中获取用户的数据,然后应该停用身份验证面板并激活用户面板。
尝试 SetActive 面板时出现此错误。
SetActive 只能从主线程调用。 加载场景时,构造函数和字段初始化程序将从加载线程中执行。 不要在构造函数或字段初始化程序中使用此函数,而是将初始化代码移至 Awake 或 Start 函数。 UnityEngine.GameObject:SetActive(Boolean)
public void SignInWithEmail()
auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task =>
DatabaseReference.GetValueAsync().ContinueWith(task =>
//here after successful signing in, it gets the data from the Database
//and after that it should activate the user panel
//and deactivate the authentication panel
//HERE IS THE PROBLEM
userPanel.SetActive(true);
authPanel.SetActive(false);
我没有尝试加载另一个场景或其他任何内容。
如果需要,我可以提供更多信息
【问题讨论】:
UI 元素只能在 UI 线程中修改,你是在另一个线程中调用 SetActive。 @Luis 那么如何在主线程中调用它呢? (我没有使用任何线程) 【参考方案1】:所以我的答案与 Milod 的 接受的答案非常相似,但有点不同,因为我花了一段时间才把头绕在他的周围,尽管他的仍然有效。
问题: 通常,您的所有代码都在 Unity 中的单个线程上运行,因为 Unity 是单线程的, 然而,当使用像 Firebase 这样需要回调的 API 时,回调函数将由新线程处理。 这可能会导致竞争条件,尤其是在 Unity 等单线程引擎上。
解决方案(来自 Unity): 从 Unity 2017.X 开始,Unity 现在需要更改 UI 组件才能在主线程(即使用 Unity 启动的第一个线程)上运行。
有什么影响?: 主要是修改 UI 的调用...
gameObject.SetActive(true); // (or false) textObject.Text = "some string" // (from UnityEngine.UI)
这与您的代码有何关系:
public void SignInWithEmail()
// auth.SignInWithEmailAndPasswordAsyn() is run on the local thread,
// ...so no issues here
auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task =>
// .ContinueWith() is an asynchronous call
// ...to the lambda function defined within the task=>
// and most importantly, it will be run on a different thread, hence the issue
DatabaseReference.GetValueAsync().ContinueWith(task =>
//HERE IS THE PROBLEM
userPanel.SetActive(true);
authPanel.SetActive(false);
-
建议的解决方案:
对于那些需要回调函数的调用,比如...
DatabaseReference.GetValueAsync()
...你可以...
将它们发送到设置为在该初始线程上运行的函数。 ...它使用队列来确保它们将按照添加的顺序运行。 ...并按照 Unity 团队建议的方式使用单例模式。实际解决方案
-
将下面的代码放置在您的场景中,该游戏对象将始终启用,这样您就有了一个...
始终在本地线程上运行
可以发送这些回调函数在本地线程上运行。
using System;
using System.Collections.Generic;
using UnityEngine;
internal class UnityMainThread : MonoBehaviour
internal static UnityMainThread wkr;
Queue<Action> jobs = new Queue<Action>();
void Awake()
wkr = this;
void Update()
while (jobs.Count > 0)
jobs.Dequeue().Invoke();
internal void AddJob(Action newJob)
jobs.Enqueue(newJob);
现在从您的代码中,您可以简单地调用...
UnityMainThread.wkr.AddJob();
...使您的代码易于阅读(和管理),如下所示...
public void SignInWithEmail()
auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task =>
DatabaseReference.GetValueAsync().ContinueWith(task =>
UnityMainThread.wkr.AddJob(() =>
// Will run on main thread, hence issue is solved
userPanel.SetActive(true);
authPanel.SetActive(false);
)
【讨论】:
您的解决方案有效。谢谢。另外,您能否确认这不会对性能造成额外开销? 嗨,Soorya。不应该有任何额外的开销。这里的问题是谁(或什么)来做这项工作。内部受信任的人(或线程)(我们知道我们将能够根据需要影响他们),而不是未经审查的“其他人”。 ..但要完成的工作量保持不变。请原谅这个类比,希望它有助于澄清问题。【参考方案2】:所以基本上 UI 元素需要在主线程中修改,我找到了这个脚本,它会在主线程中执行你的函数,只需将你的函数放在 Coroutine 和 Enqueue 到脚本(UnityMainThreadDispatcher)。 (您需要在场景中添加一个对象并将 MainThreadDispathcer 脚本添加到其中)
这是我的函数的外观:
public void SignInWithEmail()
auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task =>
DatabaseReference.GetValueAsync().ContinueWith(task =>
//Here's the fix
UnityMainThreadDispatcher.Instance().Enqueue(ShowUserPanel());
public IEnumerator ShowUserPanel()
uiController.userPanel.panel.SetActive(true);
uiController.authPanel.SetActive(false);
yield return null;
这是在 Main Thead 中运行的脚本
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
public class UnityMainThreadDispatcher : MonoBehaviour
private static readonly Queue<Action> _executionQueue = new Queue<Action>();
/// <summary>
/// Locks the queue and adds the IEnumerator to the queue
/// </summary>
/// <param name="action">IEnumerator function that will be executed from the main thread.</param>
public void Enqueue(IEnumerator action)
lock (_executionQueue)
_executionQueue.Enqueue (() =>
StartCoroutine (action);
);
/// <summary>
/// Locks the queue and adds the Action to the queue
/// </summary>
/// <param name="action">function that will be executed from the main thread.</param>
public void Enqueue(Action action)
Enqueue(ActionWrapper(action));
IEnumerator ActionWrapper(Action a)
a();
yield return null;
private static UnityMainThreadDispatcher _instance = null;
public static bool Exists()
return _instance != null;
public static UnityMainThreadDispatcher Instance()
if (!Exists ())
throw new Exception ("UnityMainThreadDispatcher could not find the UnityMainThreadDispatcher object. Please ensure you have added the MainThreadExecutor Prefab to your scene.");
return _instance;
void Awake()
if (_instance == null)
_instance = this;
DontDestroyOnLoad(this.gameObject);
public void Update()
lock(_executionQueue)
while (_executionQueue.Count > 0)
_executionQueue.Dequeue().Invoke();
void OnDestroy()
_instance = null;
【讨论】:
src: github.com/PimDeWitte/UnityMainThreadDispatcher【参考方案3】:请注意,Firebase 现在有一个很好的 ContinueWithOnMainThread
扩展方法,它比其他建议的答案更优雅地解决了这个问题:
using Firebase.Extensions;
public void SignInWithEmail()
// This code runs on the caller's thread.
auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task =>
// This code runs on an arbitrary thread.
DatabaseReference.GetValueAsync().ContinueWithOnMainThread(task =>
// This code runs on the Main thread. No problem.
userPanel.SetActive(true);
authPanel.SetActive(false);
```
【讨论】:
为其他人标记using Firebase.Extensions
在此代码 sn-p 中很重要 - 否则,它使用 system.Threading.Tasks
' 未扩展版本,只有 ContinueWith
以上是关于SetActive() 只能从主线程调用的主要内容,如果未能解决你的问题,请参考以下文章
GMSThreadException' 原因:'API 方法必须从主线程调用'
在 Apple 的 Cocoa API 中,为啥从主线程调用 NSApplicationMain 很重要?