在 iOS 中,有些情况下,应用程序可以在 Unity Editor 中完美运行,但在设备上却无法运行或无法启动。这些问题通常与代码或内容质量有关。本节将介绍最常见的情况。
出现这种情况的原因有很多。典型原因包括:
在 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 ()
此信息表明原生符号在应用程序的发行版构建过程中被剥离。可以通过以下过程获得完整的堆栈跟踪:
使用 ARM Thumb 指令集编译外部库时,通常会发生这种情况。目前,这些库不兼容 Unity。在不采用 Thumb 指令的情况下重新编译库即可轻松解决此问题。可以通过以下步骤对该库的 Xcode 项目执行此任务:
如果库来源不明,则应要求供应商提供库的非 Thumb 版本。
有时,可能会看到如下所示的消息:_Program received signal: “0”_。 这条警告消息通常不是致命的,只是表明 iOS 内存不足,并要求应用程序释放一些内存。通常,后台进程(如邮件)会释放一些内存,同时您的应用程序可以继续运行。但是,如果您的应用程序继续使用内存或要求更多的内存,操作系统最终会开始终止一些应用程序,而您的应用程序可能就是其中之一。Apple 在文献中说明多大的内存使用量是安全的,但经验观察表明,使用不到 50% 的整体设备 RAM 的应用程序不会遇到严重的内存使用量问题。 您应该依赖的主要指标是应用程序使用的 RAM 大小。应用程序内存使用量由三个主要部分构成:
注意:内部性能分析器仅显示 .NET 脚本分配的堆。总内存使用量可以通过 Xcode Instruments 来确定,如上所示。此图包括应用程序二进制文件、一些标准框架缓冲区、Unity 引擎内部状态缓冲区、.NET 运行时堆(由内部性能分析器输出的数字)、GLES 驱动程序堆和其他一些杂项内容等多个部分。
另一个工具显示应用程序进行的所有分配,并包括原生堆和托管堆统计信息。重要的统计数据是 Net bytes 的值。
为了降低内存使用量,请遵循以下原则:
向操作系统查询可用内存量看起来似乎是评估应用程序性能的好办法。但是,由于操作系统使用了大量的动态缓冲区和缓存,所以可用内存统计信息很可能不可靠。唯一可靠的方法是跟踪应用程序的内存消耗,并将其用作主要指标。注意上述工具中的图是如何随着时间的推移而变化的,特别是在加载新的关卡之后。
这种情况可能有几个原因。需要检查设备日志以获得更多详细信息。请将设备连接到 Mac,启动 Xcode,并从菜单中选择 Window > Devices and Simulators。在窗口的左侧工具栏中选择您的设备,再单击 Show the device console 按钮,然后仔细查看最新消息。此外,可能还需要调查崩溃报告。可以在以下内容中了解如何获取崩溃报告:http://developer.apple.com/iphone/library/technotes/tn2008/tn2151.html。
有关 iOS 应用程序渲染其第一帧和处理输入的时间限制,并没有完善的记录。如果应用程序超过此限制,则将被 SpringBoard 终止。例如,如果应用程序中的第一个场景太大,便会发生这种情况。为了避免这个问题,建议创建一个小的初始场景,只显示一个启动画面,使用 yield 等待一两帧,然后开始加载真实场景。可以用简单的代码来实现这一点,如下所示:
IEnumerator Start() {
yield return new WaitForEndOfFrame();
// 不要忘了使用 UnityEngine.SceneManagement 指令
SceneManager.LoadScene("Test");
}
目前,仅 .NET 2.0 Subset 配置文件支持 Type.GetProperty() 和 Type.GetValue()。可以在 Player 设置中选择 .NET API 兼容性级别。
注意:Type.GetProperty() 和 Type.GetValue() 可能与托管代码剥离不兼容,可能需要排除(可以在剥离过程中提供自定义的不可剥离类型列表来实现这一点)。有关更多详细信息,请参阅 iOS 播放器大小优化指南。
iOS 的 Mono.NET 实现基于 AOT(提前编译为原生代码)技术,这种技术自身有局限性。它只编译其他代码显式使用的泛型类型方法(其中值类型用作泛型参数)。当仅通过反射或从原生代码(即序列化系统)使用这些方法时,在 AOT 编译期间将跳过这些方法。可以通过在脚本代码中添加一个虚拟方法来提示 AOT 编译器需要包含代码。这样可以引用缺少的方法,从而提前编译这些方法。
void _unusedMethod() {
var tmp = new SomeType<SomeValueType>();
}
注意:值类型包括基本类型、枚举和结构。
.NET Cryptography 服务严重依赖反射,所以不兼容托管代码剥离,因为这涉及静态代码分析。有时,崩溃的最简单解决方案是将整个 System.Security.Crypography 命名空间从剥离过程中排除。
通过向 Unity 项目的 Assets 文件夹中添加自定义的 link.xml 文件可以自定义剥离过程。这可以指定哪些类型和命名空间应该从剥离中排除。如需了解更多详细信息,请参阅 iOS 播放器大小优化指南。
<linker>
<assembly fullname="mscorlib">
<namespace fullname="System.Security.Cryptography" preserve="all"/>
</assembly>
</linker>
应考虑以上建议,或者尝试通过在脚本代码中添加对特定类的额外引用来解决该问题:
object obj = new MD5CryptoServiceProvider();
如果使用大量递归泛型,通常会发生此错误。这种情况下可提示 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 版本中,PNG 压缩和优化工具中引入了一些更改。这些更改可能导致在 Unity iOS 运行时检查是否有启动画面修改时出现误报。如果遇到这样的问题,请尝试将 Unity 升级到最新的公开版本。如果这样做无效,可以考虑以下解决办法:
如果此方案仍然无效,请尝试在 Xcode 中禁用 PNG 重新压缩:
最常见的错误是认为 WWW 下载总是在一个单独的线程上进行。在某些平台上,这可能的确如此,但不应该认为这是理所当然的。要跟踪 WWW 状态,最佳方法是使用 yield 语句,或者在 Update 方法中检查状态。不应为此使用繁重的 while 循环。
包含 UI 的一些操作将导致 iOS 立即重绘窗口(最常见的示例是向主 UIWindow 添加带有 UIViewController 的 UIView)。如果通过脚本调用一个原生函数,则将在 Unity 的 PlayerLoop 中执行该调用,从而导致对 PlayerLoop 进行递归调用。在此类情况下,应考虑使用 performSelectorOnMainThread 方法,同时将 waitUntilDone 设置为 false。这样会要求 iOS 将操作的运行时间安排在 Unity 的 PlayerLoop 调用之间。
如果应用程序在编辑器中运行正常,但在 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 优化页面。
通常,如果托管函数委托传递给原生函数,但是在构建应用程序时未生成所需的封装器代码,则会收到这样的消息。可以向 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);
}
}
此错误通常表示单个模块中有太多的代码。通常的原因是包含大量脚本代码,或在构建中包含了大型的外部 .NET 程序集。而且,启用脚本调试可能会使情况变得更糟,因为这会为每个函数添加少量的额外指令,因此更容易达到该限制。
在 Player 设置中启用托管代码剥离可能有助于解决此问题,特别是在涉及大型外部 .NET 程序集的情况下。但是,如果问题仍然存在,那么最好的解决方案是将用户脚本代码拆分为多个程序集。最简单的方法是将一些代码移到 Plugins 文件夹。此位置的代码会放置到其他程序集。此外,还请查看有关特殊文件名称如何影响脚本编译的信息。