PAYATHON 2026

Unity 导出 Xcode 后 Firebase iOS 构建及授权失败

排障赵师傅

结论

日志中最先需要处理的是 Firebase iOS 原生组件版本混用:

Google Analytics for Firebase version (50200000)
does not match with Google App Measurement (50300000)

版本不一致会直接造成 FirebaseApp 创建失败。之后出现的 Database、Storage 和 Auth 异常,包括登录无响应、nil FIRApp 和 NullReferenceException,基本都是由此引发的连锁错误。

项目还有一个独立的初始化顺序问题:CheckDependencies 尚未完成,多个 Awake() 就开始访问 FirebaseAuth.DefaultInstance、FirebaseDatabase.DefaultInstance 和 FirebaseStorage.DefaultInstance。统一组件版本后,这种调用顺序仍可能引起偶发失败。

建议按以下顺序处理:

  1. 清除旧的 Firebase iOS 库和 CocoaPods 解析结果。
  2. 从同一版本的 Firebase Unity SDK 中重新导入全部模块。
  3. 重新解析 iOS 依赖,并生成干净的 Xcode 工程。
  4. 等 CheckAndFixDependenciesAsync() 成功后,再创建 Auth、Database 和 Storage 实例。
  5. 在登录代码中处理异步任务的成功、失败和取消结果。

日志中的错误关系

日志中最早出现的有效错误是组件版本不一致,随后出现:

InitializationException: Firebase app creation failed.

默认 Firebase App 创建失败后,数据库收到空实例:

nil FIRApp instance passed to databaseForApp
DatabaseException: Failed to get FirebaseDatabase instance.

与此同时,Storage 在依赖检查完成前开始初始化:

InvalidOperationException:
Don't call Firebase functions before CheckDependencies has finished

登录代码接着访问了初始化失败的 Auth 对象:

NullReferenceException
at Authorization.LoginUser(...)

QR code reader 和手动输入最终都会调用同一个未正确初始化的 Firebase Auth 实例,所以两种登录方式都会失败。问题并不在 QR 扫描本身。

以下日志通常与 Firebase 登录失败的根因无关:

FMOD failed to initialize the output device
Analytics screen reporting is enabled
PlayerLoop internal function has been called recursively
BoringSSL ... Lower protocol stack error

相机触发的递归 PlayerLoop 和 FMOD 音频问题仍需单独处理,不过应先修复 Firebase 初始化。

清理并统一 Firebase 依赖

FirebaseAuth.unitypackage、FirebaseDatabase.unitypackage、FirebaseStorage.unitypackage 和 FirebaseAnalytics.unitypackage 不要混用不同发布包中的版本。项目里的 Firebase Unity 模块应全部来自同一套 SDK。

先检查 Unity 项目中的以下位置:

Assets/Firebase/
Assets/Plugins/iOS/
Assets/ExternalDependencyManager/
Assets/PlayServicesResolver/

重点排查:

  • 同一个 Firebase 模块存在多个版本。
  • Assets/Plugins/iOS 中残留手动复制的 .framework、.a 或 .bundle。
  • 旧版 Play Services Resolver 和新版 External Dependency Manager 同时存在。
  • Podfile 手动固定了部分 Firebase Pod 的版本。
  • 第三方 Unity 插件另外引入了 FirebaseAnalytics 或 GoogleAppMeasurement。

清理后,从同一个 Firebase Unity SDK 发布包重新导入项目实际需要的所有模块,不要只更新其中一个组件。

接着解析 iOS 依赖。External Dependency Manager 版本不同,菜单名称也会有所差异,常见入口是:

Assets > External Dependency Manager > iOS Resolver > Force Resolve

旧版本中可能显示为:

Assets > Play Services Resolver > iOS Resolver > Resolve

重新生成 CocoaPods 依赖

建议删除旧的 Unity iOS 导出目录,然后从 Unity 重新完整导出。这样可以防止旧的 Podfile.lock、Pods 和 Xcode Build Phases 继续保留错误版本。

如果必须继续使用现有导出目录,先确认其中只有 Unity 生成的内容,没有需要保留的手工修改,再执行:

cd /path/to/exported-ios-project
pod deintegrate
rm -rf Pods
rm -f Podfile.lock
pod install --repo-update

安装完成后打开 workspace:

open YourProject.xcworkspace

不要再打开 YourProject.xcodeproj,否则 CocoaPods 添加的依赖和构建设置可能无法生效。

还要检查 Podfile.lock,确认其中没有同时存在互不兼容的 FirebaseAnalytics 和 GoogleAppMeasurement 版本。具体版本号取决于当前使用的 Firebase Unity SDK,不能根据这份旧日志强行固定某个版本。依赖解析器需要为整套 Firebase SDK 选出兼容的版本组合。

检查 iOS Firebase 配置文件

Android 使用的 google-services.json 不适用于 iOS。需要在 Firebase Console 中单独注册 iOS App,并确认注册时填写的 Bundle Identifier 与 Unity Player Settings 和 Xcode Target 完全一致。

iOS 使用的配置文件是:

GoogleService-Info.plist

检查以下内容:

  • 文件名仍为 GoogleService-Info.plist。
  • 文件来自正确的 Firebase 项目和 iOS App。
  • BUNDLE_ID 与应用实际使用的 Bundle Identifier 一致。
  • 最终 Xcode Target 的 Copy Bundle Resources 中包含该文件。
  • 工程中不存在多个来自不同 Firebase 项目的同名配置文件。

修改文件名或手工复制 Android 配置都无法解决这个问题。

统一控制 Firebase 初始化

不要让 Authorization.Awake()、Database.Awake() 和 FileDownloader.Awake() 各自抢先创建 Firebase 默认实例。可以由一个启动组件统一完成初始化,其他功能等待初始化结束。

以下示例适用于提供 Firebase.Extensions 和 ContinueWithOnMainThread 的 Firebase Unity SDK:

using System;
using Firebase;
using Firebase.Auth;
using Firebase.Database;
using Firebase.Extensions;
using Firebase.Storage;
using UnityEngine;

public sealed class FirebaseBootstrap : MonoBehaviour
{
    public static bool IsReady { get; private set; }

    public static FirebaseAuth Auth { get; private set; }
    public static FirebaseDatabase Database { get; private set; }
    public static FirebaseStorage Storage { get; private set; }

    public static event Action Ready;

    private static FirebaseBootstrap instance;

    private void Awake()
    {
        if (instance != null)
        {
            Destroy(gameObject);
            return;
        }

        instance = this;
        DontDestroyOnLoad(gameObject);

        FirebaseApp.CheckAndFixDependenciesAsync()
            .ContinueWithOnMainThread(task =>
            {
                if (task.IsCanceled)
                {
                    Debug.LogError("Firebase dependency check was canceled.");
                    return;
                }

                if (task.IsFaulted)
                {
                    Debug.LogException(task.Exception);
                    return;
                }

                DependencyStatus status = task.Result;

                if (status != DependencyStatus.Available)
                {
                    Debug.LogError(
                        "Firebase dependencies are unavailable: " + status);
                    return;
                }

                // 此时才允许访问 Firebase 默认实例。
                FirebaseApp app = FirebaseApp.DefaultInstance;

                Auth = FirebaseAuth.DefaultInstance;
                Database = FirebaseDatabase.DefaultInstance;
                Storage = FirebaseStorage.DefaultInstance;

                IsReady = true;
                Ready?.Invoke();

                Debug.Log("Firebase initialized successfully.");
            });
    }
}

如果使用的旧版 SDK 没有 ContinueWithOnMainThread,应改用该版本支持的主线程调度方式。不要直接从后台线程更新 Unity 对象或 UI。

其他脚本的 Awake() 中不要再执行这些代码:

FirebaseAuth.DefaultInstance;
FirebaseDatabase.DefaultInstance;
FirebaseStorage.DefaultInstance;

只有当 FirebaseBootstrap.IsReady 为 true 时,才能使用这些实例。

修正登录代码

QR code 和手动输入可以共用一个登录入口。调用登录方法前,先检查 Firebase 是否已经初始化:

using Firebase.Extensions;
using UnityEngine;

public sealed class Authorization : MonoBehaviour
{
    public void LoginUser(string email, string password)
    {
        if (!FirebaseBootstrap.IsReady ||
            FirebaseBootstrap.Auth == null)
        {
            Debug.LogError(
                "Firebase is not ready. Login request was not started.");
            return;
        }

        if (string.IsNullOrWhiteSpace(email) ||
            string.IsNullOrEmpty(password))
        {
            Debug.LogError("Email or password is empty.");
            return;
        }

        FirebaseBootstrap.Auth
            .SignInWithEmailAndPasswordAsync(email.Trim(), password)
            .ContinueWithOnMainThread(task =>
            {
                if (task.IsCanceled)
                {
                    Debug.LogError("Firebase sign-in was canceled.");
                    return;
                }

                if (task.IsFaulted)
                {
                    Debug.LogError("Firebase sign-in failed.");

                    if (task.Exception != null)
                    {
                        foreach (Exception exception in
                                 task.Exception.Flatten().InnerExceptions)
                        {
                            Debug.LogException(exception);
                        }
                    }

                    return;
                }

                var user = FirebaseBootstrap.Auth.CurrentUser;

                if (user == null)
                {
                    Debug.LogError(
                        "Sign-in completed but CurrentUser is null.");
                    return;
                }

                Debug.Log("Firebase sign-in succeeded: " + user.UserId);
            });
    }
}

不同版本的 Firebase Unity SDK 可能会为 SignInWithEmailAndPasswordAsync() 提供不同的返回结果类型。上面的代码通过 CurrentUser 获取登录用户,避免依赖旧版 FirebaseUser 或新版登录结果类型。

如果用户可能在 Firebase 初始化完成前点击登录,可以先禁用登录按钮,等 FirebaseBootstrap.Ready 事件触发后再启用。也可以缓存登录请求,但需要防止重复提交。

登录仍失败时检查什么

日志中不再出现版本不匹配、Firebase app creation failed 和 CheckDependencies 异常后,再检查 Auth 本身的配置:

  1. 在 Firebase Console 的 Authentication 中启用实际使用的登录方式。电子邮件登录需要启用 Email/Password。
  2. 确认账号已经创建,并检查密码是否被 QR 解析逻辑截断或附加了换行符。
  3. 记录登录任务的 task.Exception,不要只等待界面变化。
  4. 使用 Google、Apple 或其他第三方登录时,还要按照对应提供方的要求配置 URL Scheme、回调地址和 Xcode Capability。电子邮件密码登录通常不需要这些回调配置。
  5. 如果登录成功但数据库读取被拒绝,应检查 Realtime Database 或 Cloud Firestore Security Rules。这属于 authorization 问题,不是 authentication 登录失败。

如何确认修复完成

正常启动时至少应满足以下条件:

  • Xcode 日志不再报告 FirebaseAnalytics 与 GoogleAppMeasurement 版本不匹配。
  • CheckAndFixDependenciesAsync() 返回 DependencyStatus.Available。
  • 默认 FirebaseApp 只初始化一次。
  • Auth、Database 和 Storage 都在依赖检查完成后创建。
  • 登录异步任务能够明确进入成功、失败或取消分支,不再出现“点击后没有反应”。
  • Authorization.LoginUser() 和 Database.GetInitiatives() 不再抛出 NullReferenceException。

题目中的日志来自应用运行阶段,其中已经出现 applicationDidFinishLaunching() 和 applicationDidBecomeActive()。如果 Xcode 仍然存在编译或链接失败,还需要单独保留 Xcode Build Log 中的第一条红色错误。仅凭运行时日志无法判断 duplicate symbols、missing framework、签名失败等构建问题。

备注:内容仅供参考。