【解决BUG思路】get_gameObject can only be called from the main thread

2020年03月28日 10:02 0 点赞 0 评论 更新于 2025-11-21 15:02

一、BUG信息

在开发过程中,遇到如下错误信息:

11-02 14:38:46.703 6555-6934/net.lionbird.google.countryCreatorUS E/Unity: UnityException: get_gameObject can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
at BannerViewController.GetbannerPosition (System.String id, Boolean _ipx) [0x00036] in D:\projects\worldCreater\Assets\_LBEngine\_Scripts\Banner\BannerViewController.cs:221
at BannerUpdate.updateSetting () [0x00030] in D:\projects\worldCreater\Assets\_LBEngine\_Scripts\Banner\BannerUpdate.cs:29
at (wrapper delegate-invoke) System.Action:invoke_void__this__ ()
// 此处省略重复的委托调用信息

该错误表明在非主线程中调用了只能在主线程中调用的 get_gameObject 方法。

二、原因分析

此错误的根本原因是SDK线程调用了只有主线程才能调用的方法。在Unity中,部分操作(如访问 gameObject)是线程不安全的,只能在主线程中执行。当在加载场景时,构造函数和字段初始化器会在加载线程中执行,如果在这些地方使用了只能在主线程中调用的方法,就会引发该异常。

三、解决方案

为了解决这个问题,我们可以使用代理封装要执行的方法,并通过Unity的 Update 方法在主线程中调用。以下是实现该功能的代码:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

public class MainThreadCall : MonoBehaviour
{
// 用于线程同步的锁对象
static object locker = new object();
// 存储待执行的委托列表
static List<Action> aList = new List<Action>();
// 记录待执行委托的数量
static int count = 0;

// 主线程的ID
public static int MainThreadID { get; private set; }

private void Awake()
{
// 获取主线程的ID
MainThreadID = Thread.CurrentThread.ManagedThreadId;
}

// Update方法会在每一帧被调用
void Update()
{
// 检查是否有待执行的委托
if (count != 0)
{
// 加锁以确保线程安全
lock (locker)
{
// 遍历待执行的委托列表
foreach (var a in aList)
{
// 执行委托
if (a != null) a();
}
// 清空待执行的委托列表
aList.Clear();
// 重置待执行委托的数量
count = 0;
}
}
}

// 向待执行委托列表中添加委托
private static void AddCall(Action a)
{
// 加锁以确保线程安全
lock (locker)
{
// 添加委托到列表
aList.Add(a);
// 增加待执行委托的数量
count++;
}
}

// 安全回调方法,无参数
static public void SafeCallback(Action _a)
{
AddCall(_a);
}

// 安全回调方法,带一个参数
static public void SafeCallback<T>(Action<T> _a, T p1)
{
// 创建一个无参数的委托,调用带参数的委托
Action tmp = () => { _a(p1); };
AddCall(tmp);
}

// 安全回调方法,带两个参数
static public void SafeCallback<T1, T2>(Action<T1, T2> _a, T1 p1, T2 p2)
{
// 创建一个无参数的委托,调用带两个参数的委托
Action tmp = () => { _a(p1, p2); };
AddCall(tmp);
}

// 安全回调方法,带三个参数
static public void SafeCallback<T1, T2, T3>(Action<T1, T2, T3> _a, T1 p1, T2 p2, T3 p3)
{
// 创建一个无参数的委托,调用带三个参数的委托
Action tmp = () => { _a(p1, p2, p3); };
AddCall(tmp);
}

// 安全回调方法,带四个参数
static public void SafeCallback<T1, T2, T3, T4>(Action<T1, T2, T3, T4> _a, T1 p1, T2 p2, T3 p3, T4 p4)
{
// 创建一个无参数的委托,调用带四个参数的委托
Action tmp = () => { _a(p1, p2, p3, p4); };
AddCall(tmp);
}
}

代码解释:

  1. MainThreadID:用于存储主线程的ID,在 Awake 方法中初始化。
  2. locker:用于线程同步的锁对象,确保在多线程环境下对 aListcount 的操作是线程安全的。
  3. aList:存储待执行的委托列表。
  4. count:记录待执行委托的数量。
  5. Awake 方法:在脚本实例被加载时调用,获取主线程的ID。
  6. Update 方法:在每一帧被调用,检查是否有待执行的委托,如果有,则加锁遍历委托列表并执行委托,最后清空列表并重置计数。
  7. AddCall 方法:向待执行委托列表中添加委托,并增加计数。
  8. SafeCallback 系列方法:用于封装不同参数数量的委托,并调用 AddCall 方法将其添加到待执行列表中。

通过这种方式,我们可以将需要在主线程中执行的方法封装成委托,并在主线程的 Update 方法中执行,从而避免在非主线程中调用只能在主线程中执行的方法。