using System; using TapSDK.Core.Internal.Log; using TapSDK.Core.Internal.Utils; namespace TapSDK.Compliance.Internal { /// /// 合规模块调用 Unity API 的主线程闸门(UI、协程、PlayerPrefs 都走这里)。 /// /// 合规的这些调用方几乎全挂在 HTTP 响应的续体上(CheckPlayable / heartbeat / 实名认证 /// 之后)。只要 Startup 是从后台线程发起的,那条线程上没有 UnitySynchronizationContext /// 可捕获,整条 async 链的续体就都留在线程池上,此时任何 Unity API 调用都会出事: /// - Resources.Load / UIManager.OpenUI → "Graphics device is null." 原生崩溃,拦不住; /// - UIManager.Instance 首次访问 → getter 里的 Application.isPlaying / new GameObject /// 会抛 UnityException(_instance 已存在时因短路求值不会触发,所以这条是概率性的, /// 更难查); /// - CompliancePoll 的 new GameObject / StartCoroutine / StopAllCoroutines; /// - DataStorage 底层的 PlayerPrefs。 /// 因此必须把"UIManager.Instance.Xxx(...)"这类完整表达式一起送进主线程,只包住方法体 /// 内部是不够的——Instance 的访问本身就在闸门之外。 /// /// 主线程调用保持原样同步执行,既有时序一点不变;只有后台线程才改成派发。 /// /// 关于成对调用(OpenLoading/CloseLoading、StartUp/Logout)的顺序:TapLoom 的队列是 /// FIFO,所以同一线程性质下多次派发保序。跨线程混合(一端同步执行、另一端排队)理论上 /// 会乱序,但实际不会发生:后台线程发起时整条 async 链没有 SynchronizationContext, /// 续体不会自己跑回主线程,两端都会入队;主线程发起时两端都同步执行。 /// internal static class ComplianceMainThread { internal static void Run(Action action) { Run(action, null); } /// /// onError 用来把派发过程中的异常回传给等待方(通常是 TaskCompletionSource)。 /// /// 派发出去的动作必须捕获异常,原因有两个: /// 一是 TapLoom.Update 里那个 foreach 没有 try/catch,任何一个动作抛异常都会让 /// 本帧队列中其余动作全部不执行——那些动作来自各个模块,互相不该有牵连; /// 二是原本同步执行时,异常会冒泡给调用方的 try/catch,改成派发后它只会从 /// Update 里冒出来,调用方再也接不到,于是 await 那个 Task 的一方永久挂起 /// (合规的 Startup 流程就会卡死不返回)。 /// /// 没有传 onError 的 fire-and-forget 场景(Toast / Loading 之类),异常记日志 /// 即可:弹不出一个提示不该被上抛成"认证失败",更不该拖垮整帧队列。 /// internal static void Run(Action action, Action onError) { if (action == null) { return; } Action guarded = () => { try { action(); } catch (Exception e) { if (onError != null) { onError(e); } else { TapLog.Error("[Compliance] main-thread action failed: " + e); } } }; if (TapLoom.IsMainThread) { guarded(); return; } TapLoom.QueueOnMainThread(guarded); } } }