Существуют ситуации с iOS, когда ваша игра идеально работает в редакторе Unity, но работает неправильно или даже не запускается на самом устройстве. Проблемы обычно связаны с кодом или качеством контента. Этот раздел описывает самые частые варианты проблем.
Есть множество причин, почему это могло произойти. Обычно среди причин есть следующие:
Сообщение обычно высвечивается на iOS устройствах, когда ваше приложение принимает NullReferenceException. Есть 2 способа выяснить, где произошла ошибка:
Начиная с версии 3.4, в Unity существует программная обработка NullReferenceException. Компилятор AOT включает в себя быстрые проверки нулевых ссылок при каждом обращении к переменной или методу на объекте. Это функция влияет на производительность скрипта, в результате чего она включена только у сборок для разработчиков (пользователям базовой лицензии достаточно включить опцию “Development build” в диалоговом окне “Build Settings”, в то время как пользователям лицензии iOS Pro надо дополнительно включить опцию “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
Это означает, что ошибка произошла в методе handleTimeOfDay класса DayController, который работает как coroutine. И если это код скрипта, то тогда, в обычном случае, вам будет сообщён точный номер строки (например, “DayController.js:122”). Например, строка с ошибкой может выглядеть так:
Instantiate(_imgwww.assetBundle.mainAsset);
Это могло произойти, если, допустим, скрипт пытается получить доступ к набору ассетов (asset bundle) не проверив, правильно ли он загрузился.
Трассировки стека в нативной среде - гораздо более мощный инструмент для исследования ошибок, но их использование требует некоторых навыков. Кроме того, обычно приложение уже не сможет работать после возникновения таких нативных (доступ к аппаратной памяти) ошибок. Чтобы получить трассировку нативного стека, введите bt all в консоли XCode Debugger. Тщательно исследуйте полученные следы стека - они могут содержать подсказки о том, где произошла ошибка. Вы можете увидеть что-то вроде такого:
...
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”. Если внимательно присмотреться к реализации метода, то можно найти причину проблемы. Обычно, NullReferenceException происходят внутри метода Start, в результате неверных предположений о порядке запуска. В некоторых случаях только часть трассировки стека видна в консоли Debugger’а:
Thread 1 (thread 11523):
1. 0 0x0062564c in start ()
Это означает, что нативные символы были разделены во время сборки приложения. Полную трассировку стека можно получить при помощи следующей процедуры:
Обычно это происходит, когда внешняя библиотека скомпилирована с набором команд ARM Thumb. На данный момент, такие библиотеки не совместимы с Unity. Проблему можно легко решить, заново скомпилировав библиотеку без Thumb-команд. Вы можете это сделать для Xcode проекта библиотеки при помощи следующих шагов:
Если исходный код библиотеки не доступен, то вам следует попросить у поставщика/разработчика версию библиотеки без Thumb-команд.
Иногда вы можете увидеть сообщение вроде Program received signal: “0”. Это предупреждение в большинстве случаев не фатально и просто означает, что iOS не хватает памяти и система просит приложения освободить немного памяти. Обычно, фоновые процессы, вроде Mail (Почта) освободят немного памяти и ваше приложение сможет продолжить работу. Тем не менее, если ваше приложение продолжит использовать память или попросит ещё больше, то система начнёт периодически закрывать приложения и ваше может быть одним из них. Apple не упомянули, какая нагрузка на память безопасна, но основываясь на опыте можно сказать, что приложения использующие меньше 50% от максимума (грубо говоря, где-то 200–256 МБ для iPad 2) не испытывают особых проблем с использованием памяти. Главный показатель, на который вы должны полагаться, это то, сколько RAM использует ваше приложение. Использование памяти вашим приложением состоит из трёх главных компонентов:
На заметку: встроенный профайлер показывает только кучу, выделенную под .NET скрипты. Общее количество используемой памяти может быть определено при помощи Xcode Instruments, как было показано выше. Эта диаграмма включает части исполняемого файла приложения, несколько стандартных Framework-буферов, внутренние буферы состояний движка Unity, кучу среды выполнения .NET (число, выведенное внутренним профайлером), кучу GLES-драйвера и другую разнообразную информацию.
Другой инструмент показывает все выделения памяти сделанные вашим приложением, включая статистику как нативной, так и управляемой кучи (не забудьте проверить пункт Created and still living, чтобы узнать текущее состояние приложения). Важной статистикой является значение Net bytes.
Чтобы уменьшить потребление памяти:
Запрашивание объёма свободной памяти у системы может показаться хорошей идей для изучения производительности приложения. Однако, статистика свободной памяти является ненадёжной, т.к. система использует множество динамических буферов и кэшей. Единственным надёжным вариантом является отслеживание количества памяти, потребляемое вашим приложением, и использование этого в качестве главных показателей. Обратите внимание, как с течением времени меняются графики, произведённые описанными выше инструментами, особенно после загрузки новых уровней.
Для этого может быть несколько причин. Вам нужно исследовать логи устройства для получения дополнительной информации. Соедините устройство с вашем Mac, запустите Xcode и выберите Window > Organizer из меню. Выберите ваше устройство на левой панели инструментов органайзера (Organizer), затем кликните на вкладку “Console” и внимательно изучите последние сообщения. Возможно вам понадобится исследовать отчёты о крахе. Как получить отчёты о крахе, вы можете выяснить здесь: http://developer.apple.com/iphone/library/technotes/tn2008/tn2151.html.
Существует плохо описанный лимит времени для iOS приложения, отведённый для отрисовки его первых кадров и обработки ввода. Если ваше приложение превысит этот лимит, SpringBoard его закроет. Это может случиться, например, если первая сцена в приложении слишком большая. Чтобы избежать подобной проблемы, рекомендуется создать маленькую стартовую сцену, которая просто отображает заставку, ждёт кадр-другой при помощи yield, а затем начинает загружать саму сцену. Это можно сделать с помощью очень простого кода, например:
function Start() {
yield;
Application.LoadLevel("Test");
}
На данный момент Type.GetProperty() и Type.GetValue() поддерживаются только профилем .NET 2.0 Subset. Вы можете выбрать уровень совместимости .NET API в Player Settings.
На заметку: Type.GetProperty() и Type.GetValue() могут быть несовместимы со stripping’ом managed кода, что потребует их исключения (для этого во время stripping’а вы можете предоставить не подверженный ему пользовательский тип). Для дополнительной информации см. раздел Оптимизация размера собранного iOS проигрывателя.
Реализация Mono .NET для iOS основана на технологии AOT (предварительная компиляция в нативный код), которая имеет свои ограничения. Она компилирует только те методы универсальных (generic) типов (где в качестве generic параметра используется value type), которые явно используются другим кодом. Если такие методы используются только через рефлексию или из нативного кода (например, в системе сериализации), то они будут пропущены во время AOT-компиляции. AOT-компилятору можно подсказать о том, что их надо включить в компиляцию разместив где-нибудь в теле скрипта вспомогательный метод. В нём можно обратиться к пропущенным методам, тем самым форсировав их AOT-компиляцию.
void _unusedMethod() {
var tmp = new SomeType<SomeValueType>();
}
На заметку: value types - это базовые типы, структуры и перечисления.
Сервисы .NET криптографии сильно полагаются на рефлексию и поэтому не совместимы со stripping’ом managed кода, т.к. рефлексия включает в себя статический анализ кода. Иногда самым простым решением падений является исключение всего пространства имён System.Security.Crypography из процесса stripping’а.
Процесс stripping’а может быть настроен добавлением пользовательского файла link.xml в папку Assets вашего проекта Unity. Он определяет какие типы и пространства имён следует исключить из stripping’а. Дополнительную информацию можно найти в разделе Оптимизация размера собранного iOS проигрывателя.
<linker>
<assembly fullname="mscorlib">
<namespace fullname="System.Security.Cryptography" preserve="all"/>
</assembly>
</linker>
Вы можете воспользоваться описанным выше советом, или можете обойти эту проблему добавив дополнительную ссылку на определённый класс в коде вашего скрипта:
object obj = new MD5CryptoServiceProvider();
Обычно эта ошибка происходит если вы используете много рекурсивных универсальных шаблонов (они же генерики, generics). Вы можете подсказать AOT-компилятору выделять больше трамплинов типа 0, типа 1 или типа 2. Дополнительные опции командной строки AOT-компилятора можно настроить в разделе “Other Settings” настроек проигрывателя. Для трамплинов типа 1, задайте nrgctx-trampolines=ABCD, где ABCD - число необходимых новых трамплинов (т.е. 4096). Для трамплинов типа 2 задайте nimt-trampolines=ABCD и для трамплинов типа 0 задайте ntrampolines=ABCD.
В некоторых последних релизах Xcode были введены изменения в PNG сжатие и инструмент оптимизации. Эти изменения могут вызвать ложные срабатывания проверок изменения заставочного экрана в среде выполнения Unity iOS. Если вы испытываете подобные проблемы, то попробуйте обновить Unity до последней публично доступной версии. Если это не помогло, то можете попробовать обойти проблему так:
Замените свой проект Xcode с нуля при сборке из Unity (вместо присоединения)
Удалите уже установленный проект с устройства
Очистите проект в Xcode (Product->Clean)
Очистите папки производных данных Xcode (Xcode->Preferences->Locations) Если это всё равно не помогает, то попробуйте отключить PNG пережатие в Xcode:
Откройте свой проект Xcode
Выберите проект “Unity-iPhone”
Выберите вкладку “Build Settings”
Найдите опцию “Compress PNG file” и переключите в состояние NO
Такое сообщение вы можете получить когда обновляете уже существующее приложение, которое раньше было отправлено с поддержкой armv6. Unity 4.x и Xcode4.5 больше не поддерживают платформу armv6. Чтобы решить проблему отправки приложения просто смените в Unity Target OS Version (целевую версию системы) на 4.3 или выше.
Наиболее распространённой ошибкой является предположение, что процессы WWW скачивания всегда происходят в отдельном потоке. На некоторых платформах это может быть так, но вам не следует принимать это как должное. Лучший способ отследить статус WWW - либо использовать выражение yield, либо проверять статус в методе Update. Вам не следует использовать для этого циклы while.
Некоторые операции с интерфейсом приведут к немедленной перерисовке окна системой iOS (самый распространённый пример - добавление UIView вместе с UIViewController к главному UIWindow). Если вы вызовете нативную функцию из скрипта, это произойдёт внутри главного цикла проигрывателя (PlayerLoop) Unity, в результате чего PlayerLoop будет вызываться рекурсивно. В подобных случаях вам следует рассмотреть использование метода performSelectorOnMainThread с waitUntilDone = false. Это сообщит iOS, что надо поместить запуск операции между вызовами цикла проигрывателя Unity.
Если ваше приложение нормально работает в редакторе, но выдаёт ошибки в вашем iOS проекте, то это может быть вызвано отсутствующими библиотеками DLL (например, I18N.dll, I19N.West.dll). В таком случае попробуйте копировать эти DLL из Unity.app в папку Assets/Plugins вашего проекта. Расположение DLL библиотек внутри Unity.app: Unity.app/Contents/Frameworks/Mono/lib/mono/unity Вам также следует проверить уровень stripping’а вашего проекта, чтобы убедиться, что классы в DLL не были удалены при оптимизации сборки. Для большей информации касательно уровней iOS Stripping’а см. страницу по оптимизации проигрывателя iOS.
Обычно такое сообщение приходит, когда делегат managed функции передаётся в нативную функцию, но требуемый код враппера (обёртки) не был сгенерирован во время сборки приложения. Вы можете помочь 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));
}
// Use this for initialization
void Start() {
DoSomething(NoParamCallback, StringParamCallback);
}
}
Обычно эта ошибка значит что на один модуль приходится слишком много кода. В большинстве случаев это вызвано большими объёмами кода или включением больших .NET библиотек в сборку. А включение отладки скрипта может только ухудшить ситуацию, поскольку она добавляет несколько дополнительных инструкций в каждую функцию, так что с ней превысить лимит ещё проще.
Включение stripping’а managed кода в настройках проигрывателя может помочь с этой проблемой, особенно, если дело касается больших внешних .NET сборок. Но если проблема остаётся, то лучшим решением будет разделение пользовательского кода на несколько библиотек. Самым простой способ - переместить некоторую часть кода в папку Plugins. Расположенный там код будет помещён в отдельную библиотеку. Также изучите информацию о том, как имена особых папок влияют на компиляцию скрипта.