Unity 에디터에서는 게임이 완벽하게 동작했으나 실제 iOS 기기에서는 제대로 동작하지 않거나 심지어 아예 실행되지 않을 경우가 있습니다. 이러한 문제는 코드 또는 콘텐츠 품질과 관련이 있는 경우가 있습니다. 이 섹션에서는 가장 일반적인 시나리오에 대해 다룹니다.
이 현상이 발생하는 이유는 다양합니다. 일반적인 원인은 다음과 같습니다.
이 메시지는 일반적으로 애플리케이션이 NullReferenceException을 받았을 때 iOS 기기에서 나타납니다. 문제가 발생한 지점을 알아내기 위한 방법은 두 가지가 있습니다.
Unity는 NullReferenceException의 소프트웨어 기반 핸들링을 포함합니다. AOT 컴파일러는 오브젝트가 어떤 메서드나 변수에 액세스할 때마다 null 참조 여부를 빨리 확인하는 기능을 포함합니다. 이 기능은 스크립트 성능에 영향을 주므로 개발 빌드에만 포함해야 합니다(빌드 설정 다이얼로그에서 “스크립트 디버깅” 옵션 활성화). 문제가 없고 오류가 실제로는 .NET 코드에서 발생한다면 EXC_BAD_ACCESS가 더 이상 발생하지 않습니다. 그 대신 Xcode 콘솔에 .NET 예외 텍스트가 출력됩니다(또는 작성한 코드에서 이 예외를 “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);
이 문제는 예를 들어 에셋 번들이 제대로 다운로드 되었는지 먼저 확인하지 않고 스크립트에서 에셋 번들에 액세스했을 때 발생할 수 있습니다.
네이티브 스택 추적은 오류 검사에 있어 더 강력한 툴이지만, 이 방법을 사용하려면 전문 지식이 다소 필요합니다. 또한 일반적으로 이러한 네이티브(하드웨어 메모리 액세스) 폴트가 일어난 후에는 더 이상 진행할 수 없습니다. 네이티브 스택 추적을 하려면 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”의 스택 트레이스를 찾아야 합니다. 스택 트레이스의 맨 첫 줄은 오류가 발생한 지점을 가리킵니다. 이 예에서 트레이스가 나타내는 바는 “OptionsMenu” 스크립트의 “Start” 메서드에서 NullReferenceException이 발생했다는 것입니다. 이 메서드 구현을 잘 살펴 보면 문제의 원인이 밝혀질 수도 있습니다. 일반적으로, NullReferenceExceptions은 Start 메서드에서 일어나며 초기화 순서에 대해 잘못된 추정이 이루어졌을 때 발생합니다. 어떤 경우에는 디버거 콘솔에 스택 트레이스의 일부만이 나타날 경우도 있습니다.
Thread 1 (thread 11523):
1. 0 0x0062564c in start ()
이 메시지는 애플리케이션의 릴리스 빌드 중에 네이티브 심볼이 삭제되었음을 의미합니다. 다음 과정을 통해 풀 스택 트레이스를 얻을 수 있습니다.
이 현상은 보통 외부 라이브러리가 ARM Thumb 명령어 세트로 컴파일되었을 때 발생합니다. 현재 이러한 라이브러리는 Unity와 호환되지 않습니다. 이 문제는 라이브러리를 Thumb 명령어 없이 재컴파일하여 간단히 해결할 수 있습니다. 해당 라이브러리의 Xcode 프로젝트에서 다음 단계를 거치면 됩니다:
라이브러리 소스를 사용할 수 없다면 해당 라이브러리 공급자에게 thumb을 사용하지 않은 버전을 요청해야 합니다.
(때로는 Program received signal: “0” 과 같은 메시지가 나타날 수도 있습니다.) 이 경고 메시지는 치명적이지 않은 경우가 많으며, 그저 iOS의 메모리가 부족하므로 애플리케이션에서 메모리를 좀 해제해 달라는 요청의 의미입니다. 일반적으로 메일과 같은 백그라운드 프로세스가 메모리를 일부 해제할 것이며 작성한 애플리케이션을 계속 실행할 수 있습니다. 그러나, 이 애플리케이션이 계속 메모리를 사용하거나 추가로 요청한다면 OS에서는 결과적으로 애플리케이션을 강제종료하기 시작하며, 현재 사용 중인 애플리케이션도 그 대상이 될 수 있습니다. Apple은 어느 정도의 메모리 사용이 안전한지에 대해 명시하지 않고 있지만 경험적으로 볼 때 전체 기기 RAM(2세대 iPad의 경우 대략 200256 MB)의 50% 이하로 메모리를 사용하는 애플리케이션은 메모리 사용량 문제를 크게 겪지 않습니다. 여기서 측정해야 하는 것은 작성한 애플리케이션이 RAM을 얼마나 사용하는지입니다. 애플리케이션 메모리 사용량은 크게 세 가지로 나누어 볼 수 있습니다.
참고: 내부 프로파일러는 .NET 스크립트에서 할당한 힙만을 보여 줍니다. 전체 메모리 사용량은 위에 나타난 것처럼 Xcode Instruments를 통하여 판단할 수 있습니다. 이 수치는 애플리케이션 바이너리 파트, 일부 스탠다드 프레임워크 버퍼, Unity 엔진 내부 스테이트 버퍼, .NET 런타임 힙(내부 프로파일러에 의해 출력된 숫자), GLES 드라이버 힙, 그 외 일부 다른 것을 포함합니다.
다른 툴은 애플리케이션이 할당한 모든 내역을 보여 주며 네이티브 힙 및 매니지드 힙 통계를 모두 포함합니다(애플리케이션의 현재 상태를 확인하기 위해 Created and still living 상자도 잊지 말고 확인합니다). 여기서 중요한 통계는 Net bytes 값입니다.
메모리 사용량을 낮게 유지하려면
사용 가능한 메모리 양을 OS에 쿼리하는 것은 애플리케이션 성능을 측정하는 데 있어 좋은 방법처럼 보입니다. 그러나 OS가 대량의 동적 버퍼와 캐시를 사용하기 때문에 사용 가능 메모리 통계를 신뢰하기 쉽지 않습니다. 신뢰할 수 있는 유일한 접근법은 작성한 애플리케이션의 메모리 소비량을 계속 추적하고 이를 주 기준으로 삼는 것입니다. 위에서 설명한 툴을 통해 그래프가 시간에 따라 어떻게 변하는지, 특히 새 레벨을 로드한 후의 변화에 주의를 기울이십시오.
여러 가지 원인이 있을 수 있습니다. 기기 로그를 확인하여 더 구체적인 상황을 파악해야 합니다. 기기를 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 호환성 레벨은 플레이어 설정에서 선택할 수 있습니다.
참고: Type.GetProperty() 및 Type.GetValue() 는 매니지드 코드 스트리핑과 호환되지 않을 수 있으며 제외되어야 할 수도 있습니다(이를 위해 스트리핑 프로세스 중에 직접 작성한 커스텀 스트립 불가 타입 리스트를 사용할 수도 있습니다). 자세한 내용은 iOS 플레이어 크기 최적화 가이드를 참조하십시오.
iOS용 Mono .NET 구현은 AOT(네이티브 코드로 사전 컴파일) 기술에 기반하며 제한이 있습니다. 오직 다른 코드에서 명시적으로 사용되는 제네릭 타입 메서드(값 타입이 제네릭 파라미터로 사용됨)만을 컴파일합니다. 이러한 메서드가 반사를 통해 또는 네이티브 코드(즉, 직렬화 시스템)에서만 사용될 경우 이 메서드는 AOT 컴파일에서 제외됩니다. 더미 메서드를 스크립트 코드 어딘가에 추가하는 방식으로 AOT 컴파일러에 코드를 추가하도록 힌트를 줄 수 있습니다. 이렇게 하면 누락되는 메서드를 지목하여 사전에 컴파일되도록 할 수 있습니다.
void _unusedMethod() {
var tmp = new SomeType<SomeValueType>();
}
참고: 값 타입은 기본 타입, enum, struct입니다.
.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을 더 많이 할당하라고 알려주는 방법을 사용할 수 있습니다. 추가 AOT 컴파일러 커맨드 라인 옵션은 플레이어 설정의 “기타 설정” 섹션에서 지정할 수 있습니다. 타입 1 trampoline의 경우 nrgctx-trampolines=ABCD 라 지정해야 합니다. 이 때 ABCD는 요구되는 새 trampoline의 수입니다(예: 4096). 타입 2 trampoline은 nimt-trampolines=ABCD, 타입 0은 ntrampolines=ABCD 으로 지정해야 합니다.
일부 최신 Xcode 릴리즈의 경우 PNG 압축 및 최적화 툴에 변경 사항이 있습니다. 이 변경 사항은 Unity iOS 런타임이 스플래시 화면 수정사항을 확인할 때 참값 오류를 발생시킬 수 있습니다. 이러한 문제가 발생했다면 Unity를 공개된 사용 가능 최신 버전으로 업그레이드해 보십시오. 문제가 해결되지 않으면 다음 조치를 취해 보십시오.
Unity에서 빌드할 때 Xcode 프로젝트를 (덧붙이는 대신) 완전히 처음부터 교체해 보십시오.
이미 설치된 프로젝트를 장치에서 삭제해야 합니다.
Xcode에서 프로젝트를 클린하십시오 (Product->Clean)
Xcode의 Derived Data 폴더를 비우십시오(Xcode->Preferences->Locations) 이 문제가 여전히 해결되지 않으면 Xcode에서 PNG 재압축을 비활성화할 수 있습니다.
Xcode 프로젝트를 엽니다.
여기서 “Unity-iPhone” 프로젝트를 선택합니다.
“빌드 설정” 탭을 선택합니다.
“PNG 파일 압축”을 찾아 NO로 설정합니다.
위와 같은 메시지는 armv6 지원을 포함하여 이전에 제출된 적이 있는 이미 존재하는 애플리케이션을 업데이트할 때 발생할 수 있습니다. Unity 4.x와 Xcode 4.5는 armv6 플랫폼을 더 이상 지원하지 않습니다. 제출 문제를 해결하려면 Unity 플레이어 설정 에서 Target OS Version 을 4.3 이상 버전으로 설정해야 합니다.
가장 흔한 실수는 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 폴더로 복사해 보십시오. Unity app의 DLL 위치는 다음과 같습니다: 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));
}
// Use this for initialization
void Start() {
DoSomething(NoParamCallback, StringParamCallback);
}
}
이 오류는 보통 하나의 모듈에 너무 많은 코드가 있다는 의미입니다. 일반적으로 빌드에 대량의 스크립트 코드 또는 대형 외부 .NET 어셈블리가 포함된 경우 발생합니다. 그리고 스크립트 디버깅을 활성화하면 상황이 악화될 수 있습니다. 왜냐하면 이로 인해 상당한 양의 추가 명령어가 각 함수에 추가되므로, 제한에 도달하기 쉬워지기 때문입니다.
플레이어 설정에서 매니지드 코드 스트리핑을 활성화하면(특히 대형 외부 .NET 어셈블리가 연관된 경우) 이 문제 해결에 도움이 될 수 있습니다. 그러나 문제가 여전히 발생한다면 최선의 해결책은 사용자 스크립트 코드를 여러 개의 어셈블리로 분할합니다. 가장 쉬운 방법은 일부 코드를 Plugins 폴더로 옮기는 것입니다. 이 위치에 있는 코드는 다른 어셈블리에 포함됩니다. 또한 특수 폴더명이 스크립트 컴파일에 미치는 영향에 대한 정보도 확인해 보십시오.