Version: 2020.3
Social API
报告 iOS 上的崩溃错误

在 iOS 设备上进行故障排除

在 iOS 中,有些情况下,应用程序可以在 Unity Editor 中完美运行,但在设备上却无法运行或无法启动。这些问题通常与代码或内容质量有关。本节将介绍最常见的情况。

游戏在片刻之后停止响应。Xcode 在状态栏中显示“interrupted”。

出现这种情况的原因有很多。典型原因包括:

  • 脚本错误,例如使用未初始化的变量等。
  • 使用了第三方 Thumb 编译的原生库。这些库会在 iOS SDK 链接器中触发一个已知问题,并可能导致随机崩溃。
  • 为可序列化的脚本属性使用了带有值类型的通用类型作为参数(例如 List<int>、List<SomeStruct>、List<SomeEnum>)。
  • 在启用托管代码剥离的情况下使用了反射。
  • 原生插件接口中出错(托管代码方法签名与原生代码函数签名不匹配)。 来自 XCode 调试器控制台的信息通常有助于检测这些问题。(Xcode 菜单:__View > Debug Area > Activate Console__)。

Xcode 控制台显示 Program received signal: “SIGBUS” 或 EXC_BAD_ACCESS 错误。

在 iOS 设备上,当应用程序收到 NullReferenceException 时,通常会出现此消息。有两种方法可以找出发生故障的位置:

托管堆栈跟踪

Unity 可以对 NullReferenceException 进行基于软件的处理。每次在对象中访问某个方法或变量时,AOT 编译器都会快速检查 null 引用。此功能会影响脚本性能,这就是只为开发版本启用此功能的原因(在 Build Settings 对话框中启用 script debugging 选项)。 如果所有操作均正确完成,且该错误实际发生在 .NET 代码中,则不会再看到 EXC_BAD_ACCESS。相反,.NET 异常文本将输出到 Xcode 控制台中(否则,您的代码将只在“catch”语句中处理此异常)。典型的输出可能为:

Unhandled Exception: System.NullReferenceException: A null value was found where an object instance was required.
  at DayController+$handleTimeOfDay$121+$.MoveNext () [0x0035a] in DayController.js:122

此信息表明该故障出现在 DayController 类的 handleTimeOfDay 方法中(用作协程)。另外,如果是脚本代码,通常会告知您确切的行号(例如“DayController.js:122”)。有问题的行可能如下所示:

 Instantiate(_imgwww.assetBundle.mainAsset);

比如,如果脚本没有事先检查其 Asset Bundle 是否下载正确而直接访问该 Asset Bundle,则可能会发生这种情况。

原生堆栈跟踪

原生堆栈跟踪是一种更强大的故障调查工具,但使用时需要一些专门知识。此外,在这些原生(硬件内存访问)错误发生后,通常无法继续。要获取原生堆栈跟踪,请在 Xcode 调试器控制台中键入 bt all。仔细检查输出的堆栈跟踪;这些堆栈跟踪可能包含有关错误发生位置的提示。可能会看到如下内容:

...
Thread 1 (thread 11523): 

1.0 0x006267d0 in m_OptionsMenu_Start ()
1.1 0x002e4160 in wrapper_runtime_invoke_object_runtime_invoke_void__this___object_intptr_intptr_intptr ()
1.2 0x00a1dd64 in mono_jit_runtime_invoke (method=0x18b63bc, obj=0x5d10cb0, params=0x0, exc=0x2fffdd34) at /Users/mantasp/work/unity/unity-mono/External/Mono/mono/mono/mini/mini.c:4487
1.3 0x0088481c in MonoBehaviour::InvokeMethodOrCoroutineChecked ()
...

首先,应该查找“Thread 1”(也就是主线程)的堆栈跟踪。该堆栈跟踪的第一行将指向错误发生的位置。在本示例中,该跟踪表明 NullReferenceException 发生在 OptionsMenu 脚本的 Start 方法中。仔细查看此方法实现情况将揭示问题的原因。通常,如果对初始化顺序做出不正确的判断,_Start_ 方法中就会发生 NullReferenceExceptions。 在某些情况下,调试器控制台上仅显示部分堆栈跟踪:

Thread 1 (thread 11523): 

1.0 0x0062564c in start ()

此信息表明原生符号在应用程序的发行版构建过程中被剥离。可以通过以下过程获得完整的堆栈跟踪:

  • 从设备上删除应用程序。
  • 清除所有目标。
  • 构建并运行。
  • 如前所述,再次获取堆栈跟踪。

当外部库链接到 Unity iOS 应用程序时,便开始发生 EXC_BAD_ACCESS。

使用 ARM Thumb 指令集编译外部库时,通常会发生这种情况。目前,这些库不兼容 Unity。在不采用 Thumb 指令的情况下重新编译库即可轻松解决此问题。可以通过以下步骤对该库的 Xcode 项目执行此任务:

  • 在 Xcode 中,从菜单中选择 View > Navigators > Show Project Navigator
  • 选择 Unity-iPhone 项目,然后激活 Build Settings 选项卡
  • 在搜索字段中输入:Other C Flags
  • 在其中添加 -mno-thumb 标志,然后重新构建库。

如果库来源不明,则应要求供应商提供库的非 Thumb 版本。

Xcode 控制台显示“WARNING -> applicationDidReceiveMemoryWarning()”,而且应用程序随后立即崩溃

有时,可能会看到如下所示的消息:_Program received signal: “0”_。 这条警告消息通常不是致命的,只是表明 iOS 内存不足,并要求应用程序释放一些内存。通常,后台进程(如邮件)会释放一些内存,同时您的应用程序可以继续运行。但是,如果您的应用程序继续使用内存或要求更多的内存,操作系统最终会开始终止一些应用程序,而您的应用程序可能就是其中之一。Apple 在文献中说明多大的内存使用量是安全的,但经验观察表明,使用不到 50% 的整体设备 RAM 的应用程序不会遇到严重的内存使用量问题。 您应该依赖的主要指标是应用程序使用的 RAM 大小。应用程序内存使用量由三个主要部分构成:

  • 应用程序代码(操作系统需要在 RAM 中加载和保留应用程序代码,但如果确实需要,可能会丢弃其中一些代码)
  • 原生堆(由引擎用于在 RAM 中存储其状态、资源等)
  • 托管堆(由 Mono 运行时用于保留 C# 对象)
  • GLES 驱动程序内存池:纹理、帧缓冲区、编译后的着色器等。 应用程序内存使用量可通过以下两个 Xcode Instruments 工具进行跟踪:__Activity Monitor__ 和 Allocations。可以从 Xcode Run 菜单中启动:__Product > Profile__,然后选择特定工具,或者通过 Xcode > Open Developer Tools > Instruments 来选择。Activity Monitor 工具显示所有进程统计数据,包括__真实内存 (Real memory)__,这可以被视为应用程序使用的 RAM 总量。注意:操作系统和设备硬件版本组合可能会显著影响内存使用量,因此在比较不同设备上获得的数字时,应该谨慎。

注意:内部性能分析器仅显示 .NET 脚本分配的堆。总内存使用量可以通过 Xcode Instruments 来确定,如上所示。此图包括应用程序二进制文件、一些标准框架缓冲区、Unity 引擎内部状态缓冲区、.NET 运行时堆(由内部性能分析器输出的数字)、GLES 驱动程序堆和其他一些杂项内容等多个部分。

另一个工具显示应用程序进行的所有分配,并包括原生堆和托管堆统计信息。重要的统计数据是 Net bytes 的值。

为了降低内存使用量,请遵循以下原则:

  • 使用最强大的 iOS 剥离选项减少应用程序二进制文件大小,并避免对不同 .NET 库的不必要依赖。请参阅 Player 设置和优化构建的 iOS 播放器的大小以了解更多详细信息。
  • 减小内容的大小。对纹理进行 PVRTC 压缩并使用简单多边形模型。请参阅有关减小文件大小的手册页以了解更多信息。
  • 不要在脚本中分配超过必要数量的内存。使用内部性能分析器来跟踪 Mono 堆大小和使用情况。

向操作系统查询可用内存量看起来似乎是评估应用程序性能的好办法。但是,由于操作系统使用了大量的动态缓冲区和缓存,所以可用内存统计信息很可能不可靠。唯一可靠的方法是跟踪应用程序的内存消耗,并将其用作主要指标。注意上述工具中的图是如何随着时间的推移而变化的,特别是在加载新的关卡之后。

游戏从 Xcode 启动时运行正常,但在设备上手动启动时,加载第一个关卡的过程中发生崩溃。

这种情况可能有几个原因。需要检查设备日志以获得更多详细信息。请将设备连接到 Mac,启动 Xcode,并从菜单中选择 Window > Devices and Simulators。在窗口的左侧工具栏中选择您的设备,再单击 Show the device console 按钮,然后仔细查看最新消息。此外,可能还需要调查崩溃报告。可以在以下内容中了解如何获取崩溃报告:http://developer.apple.com/iphone/library/technotes/tn2008/tn2151.html

Xcode Organizer 控制台包含消息“killed by SpringBoard”。

有关 iOS 应用程序渲染其第一帧和处理输入的时间限制,并没有完善的记录。如果应用程序超过此限制,则将被 SpringBoard 终止。例如,如果应用程序中的第一个场景太大,便会发生这种情况。为了避免这个问题,建议创建一个小的初始场景,只显示一个启动画面,使用 yield 等待一两帧,然后开始加载真实场景。可以用简单的代码来实现这一点,如下所示:

IEnumerator Start() {
    yield return new WaitForEndOfFrame();
// 不要忘了使用 UnityEngine.SceneManagement 指令
    SceneManager.LoadScene("Test");
}

Type.GetProperty() 或 Type.GetValue() 导致设备上发生崩溃

目前,仅 .NET 2.0 Subset 配置文件支持 Type.GetProperty()Type.GetValue()。可以在 Player 设置中选择 .NET API 兼容性级别。

注意:Type.GetProperty()Type.GetValue() 可能与托管代码剥离不兼容,可能需要排除(可以在剥离过程中提供自定义的不可剥离类型列表来实现这一点)。有关更多详细信息,请参阅 iOS 播放器大小优化指南

游戏崩溃并显示错误消息“ExecutionEngineException: Attempting to JIT compile method ‘SometType`1<SomeValueType>:.ctor ()’ while running with –aot-only.”

iOS 的 Mono.NET 实现基于 AOT(提前编译为原生代码)技术,这种技术自身有局限性。它只编译其他代码显式使用的泛型类型方法(其中值类型用作泛型参数)。当仅通过反射或从原生代码(即序列化系统)使用这些方法时,在 AOT 编译期间将跳过这些方法。可以通过在脚本代码中添加一个虚拟方法来提示 AOT 编译器需要包含代码。这样可以引用缺少的方法,从而提前编译这些方法。

void _unusedMethod() {
    var tmp = new SomeType<SomeValueType>();
}

注意:值类型包括基本类型、枚举和结构。

将 System.Security.Cryptography 与托管代码剥离组合使用时,设备上发生各种崩溃

.NET Cryptography 服务严重依赖反射,所以不兼容托管代码剥离,因为这涉及静态代码分析。有时,崩溃的最简单解决方案是将整个 System.Security.Crypography 命名空间从剥离过程中排除。

通过向 Unity 项目的 Assets 文件夹中添加自定义的 link.xml 文件可以自定义剥离过程。这可以指定哪些类型和命名空间应该从剥离中排除。如需了解更多详细信息,请参阅 iOS 播放器大小优化指南

link.xml

<linker>
       <assembly fullname="mscorlib">
               <namespace fullname="System.Security.Cryptography" preserve="all"/>
       </assembly>
</linker>

将 System.Security.Cryptography.MD5 与托管代码剥离一起使用时,应用程序崩溃

应考虑以上建议,或者尝试通过在脚本代码中添加对特定类的额外引用来解决该问题:

object obj = new MD5CryptoServiceProvider();

“Ran out of trampolines of type 0/1/2”运行时错误

如果使用大量递归泛型,通常会发生此错误。这种情况下可提示 AOT 编译器分配更多类型 0、类型 1或类型 2 的 trampoline。可以在 Player 设置的 Other Settings 部分中指定其他 AOT 编译器命令行选项。对于类型 0 的 trampoline,请指定 ntrampolines=ABCD,其中 ABCD 表示所需的新 trampoline 的数量(例如 4096)。对于类型 1 的 trampoline,请指定 nrgctx-trampolines=ABCD,对于类型 2 的 trampoline,请指定 nimt-trampolines=ABCD

在升级了 Xcode Unity iOS 之后,运行时失败,并显示消息“You are using Unity iPhone Basic.You are not allowed to remove the Unity splash screen from your game”

在一些最新的 Xcode 版本中,PNG 压缩和优化工具中引入了一些更改。这些更改可能导致在 Unity iOS 运行时检查是否有启动画面修改时出现误报。如果遇到这样的问题,请尝试将 Unity 升级到最新的公开版本。如果这样做无效,可以考虑以下解决办法:

  • 从 Unity 进行构建时,请从头开始替换 Xcode 项目(而不是进行追加)
  • 从设备中删除已安装的项目
  • 在 Xcode 中清理项目 (Product > Clean)
  • 清除 Xcode 的 Derived Data 文件夹 (Xcode > Preferences > Locations)

如果此方案仍然无效,请尝试在 Xcode 中禁用 PNG 重新压缩:

  • 打开您的 Xcode 项目
  • 选择 Unity-iPhone 项目
  • 选择 Build Settings 选项卡
  • 查找 Compress PNG files 选项并设置为 NO

WWW 下载在 Unity Editor 和 Android 中正常工作,但在 iOS 中无效

最常见的错误是认为 WWW 下载总是在一个单独的线程上进行。在某些平台上,这可能的确如此,但不应该认为这是理所当然的。要跟踪 WWW 状态,最佳方法是使用 yield 语句,或者在 Update 方法中检查状态。不应为此使用繁重的 while 循环。

通过从脚本调用的原生函数来使用 Cocoa 时,发生“PlayerLoop called recursively!”错误

包含 UI 的一些操作将导致 iOS 立即重绘窗口(最常见的示例是向主 UIWindow 添加带有 UIViewController 的 UIView)。如果通过脚本调用一个原生函数,则将在 Unity 的 PlayerLoop 中执行该调用,从而导致对 PlayerLoop 进行递归调用。在此类情况下,应考虑使用 performSelectorOnMainThread 方法,同时将 waitUntilDone 设置为 false。这样会要求 iOS 将操作的运行时间安排在 Unity 的 PlayerLoop 调用之间。

性能分析器或调试器无法看到游戏在 iOS 设备上运行

  • 检查是否进行了开发构建,并且选中了 Script DebuggingAutoconnect Profiler 框(视情况而定)。
  • 在设备上运行的应用程序将在 UDP 端口 54997 上向 225.0.0.222 进行多播广播。请检查您的网络设置是否允许此流量。然后,性能分析器将连接到远程设备上 55000 到 55511 范围内的端口,以便从设备中获取性能分析器数据。需要打开这些端口来进行 TCP 访问。

缺少 DLL

如果应用程序在编辑器中运行正常,但在 iOS 项目中出现错误,这可能是由于缺少 DLL(例如,I18N.dll、I19N.West.dll)造成的。在这种情况下,请尝试将这些 dll 从 Unity.app 复制到项目的 Assets\Plugins 文件夹中。DLL 在 Unity 应用程序中的位置为: Unity.app\Contents\Frameworks\Mono\lib\mono\unity 然后,还应该检查项目的剥离级别,以确保在优化构建时 DLL 中的类不会被删除。有关 iOS 剥离级别的更多信息,请参阅 iOS 优化页面

Xcode 调试器控制台报告:ExecutionEngineException: Attempting to JIT compile method ‘(wrapper native-to-managed) Test:TestFunc (int)’ while running with –aot-only

通常,如果托管函数委托传递给原生函数,但是在构建应用程序时未生成所需的封装器代码,则会收到这样的消息。可以向 AOT 编译器提示哪些方法将作为委托传递给原生代码。为此,可以添加自定义属性 MonoPInvokeCallbackAttribute。目前,只能将静态方法作为委托传递给原生代码。

代码示例:

using UnityEngine;
using System.Collections;
using System;
using System.Runtime.InteropServices;
using AOT;

public class NewBehaviourScript : MonoBehaviour {
    [DllImport ("__Internal")]
    private static extern void DoSomething (NoParamDelegate del1, StringParamDelegate del2);

    delegate void NoParamDelegate ();
    delegate void StringParamDelegate (string str);
    
    [MonoPInvokeCallback(typeof(NoParamDelegate))]
    public static void NoParamCallback() {
        Debug.Log ("Hello from NoParamCallback");
    }
    
    [MonoPInvokeCallback(typeof(StringParamDelegate))]
    public static void StringParamCallback(string str) {
        Debug.Log(string.Format("Hello from StringParamCallback {0}", str));
    }

    // 用于进行初始化
    void Start() {
        DoSomething(NoParamCallback, StringParamCallback);
    }
}

Xcode 抛出编译错误:“ld : unable to insert branch island.No insertion point available. for architecture armv7”、“clang: error: linker command failed with exit code 1 (use -v to see invocation)”

此错误通常表示单个模块中有太多的代码。通常的原因是包含大量脚本代码,或在构建中包含了大型的外部 .NET 程序集。而且,启用脚本调试可能会使情况变得更糟,因为这会为每个函数添加少量的额外指令,因此更容易达到该限制。

Player 设置中启用托管代码剥离可能有助于解决此问题,特别是在涉及大型外部 .NET 程序集的情况下。但是,如果问题仍然存在,那么最好的解决方案是将用户脚本代码拆分为多个程序集。最简单的方法是将一些代码移到 Plugins 文件夹。此位置的代码会放置到其他程序集。此外,还请查看有关特殊文件名称如何影响脚本编译的信息。


  • 2018–06–14 页面已修订
Social API
报告 iOS 上的崩溃错误