Unity 에디터에서는 게임이 완벽하게 동작했으나 실제 iOS 기기에서는 제대로 동작하지 않거나 심지어 아예 실행되지 않을 경우가 있습니다. 이러한 문제는 코드 또는 콘텐츠 품질과 관련이 있는 경우가 있습니다. 이 섹션에서는 가장 일반적인 시나리오에 대해 다룹니다.
이 현상이 발생하는 이유는 다양합니다. 일반적인 원인은 다음과 같습니다.
일반적으로 이 메시지는 애플리케이션이 NullReferenceException을 수신할 때 iOS 기기에 나타납니다. 발생한 오류는 다음의 두 가지 방법으로 파악할 수 있습니다.
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);
이 문제는 예를 들어 에셋 번들이 제대로 다운로드 되었는지 먼저 확인하지 않고 스크립트에서 에셋 번들에 액세스했을 때 발생할 수 있습니다.
네이티브 스택은 오류 조사를 위한 매우 강력한 툴이지만, 약간의 전문 지식이 있어야 사용할 수 있습니다. 또한 이러한 네이티브(하드웨어 메모리 액세스) 오류가 일어난 후에는 대개 작업을 계속할 수 없습니다. 네이티브 스택 추적을 확인하려면 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 메서드 내에서 발생했습니다. 이 메서드 구현을 자세히 살펴보면 문제의 원인을 밝혀낼 수 있습니다. 일반적으로 NullReferenceException은 초기화 순서에 대한 잘못된 가정이 있을 때 Start 메서드 내에서 발생합니다. 일부의 경우 스택 추적 내용이 부분적으로만 디버거 콘솔에 표시됩니다.
Thread 1 (thread 11523):
1. 0 0x0062564c in start ()
이 메시지는 애플리케이션의 릴리스 빌드 중에 네이티브 심볼이 삭제되었음을 의미합니다. 다음 과정을 통해 풀 스택 트레이스를 얻을 수 있습니다.
일반적으로 외부 라이브러리가 ARM Thumb 명령 집합으로 컴파일될 때 발생합니다. 현재 이러한 라이브러리는 Unity와 호환되지 않습니다. 이 문제는 Thumb 명령 없이 라이브러리를 다시 컴파일하면 쉽게 해결할 수 있습니다. 다음의 단계에 따라 라이브러리의 Xcode 프로젝트에 대해 이 작업을 수행할 수 있습니다.
라이브러리 소스를 사용할 수 없다면 해당 라이브러리 공급자에게 thumb을 사용하지 않은 버전을 요청해야 합니다.
때때로 Program received signal: “0” 같은 메시지가 표시될 수 있습니다. 이 경고 메시지는 치명적이지는 않지만, iOS에 메모리가 부족함을 표시하고 애플리케이션이 추가 메모리를 확보하도록 요청합니다. 대개 메일 같은 백그라운드 프로세스의 메모리를 비우면 애플리케이션을 계속 실행할 수 있습니다. 하지만 애플리케이션이 메모리를 계속 사용하고 추가 메모리를 요구하면 OS는 현재 사용 중인 것을 포함하여 애플리케이션을 종료합니다. Apple은 안전한 메모리 사용에 관해 문서화하지 않았지만, 경험으로 비추어볼 때 전체 기기 RAM의 50% 미만을 사용하는 애플리케이션은 심각한 메모리 사용 문제를 겪지 않습니다. 이때 애플리케이션이 사용하는 RAM 양에 관한 지표를 사용해야 합니다. 애플리케이션 메모리 사용은 다음 세 가지 주요 컴포넌트로 구성됩니다.
참고: 내부 프로파일러는 .NET 스크립트에서 할당한 힙만을 보여 줍니다. 전체 메모리 사용량은 위에 나타난 것처럼 Xcode Instruments를 통하여 판단할 수 있습니다. 이 수치는 애플리케이션 바이너리 파트, 일부 스탠다드 프레임워크 버퍼, Unity 엔진 내부 스테이트 버퍼, .NET 런타임 힙(내부 프로파일러에 의해 출력된 숫자), GLES 드라이버 힙, 그 외 일부 다른 것을 포함합니다.
다른 툴은 애플리케이션의 모든 할당량을 표시하며, 여기에는 네이티브 힙과 관리되는 힙 통계가 모두 포함됩니다. 중요한 통계는 Net bytes 값입니다.
메모리 사용량을 낮게 유지하려면
사용 가능한 메모리 양을 OS에 쿼리하는 것은 애플리케이션 성능을 측정하는 데 있어 좋은 방법처럼 보입니다. 그러나 OS가 대량의 동적 버퍼와 캐시를 사용하기 때문에 사용 가능 메모리 통계를 신뢰하기 쉽지 않습니다. 신뢰할 수 있는 유일한 접근법은 작성한 애플리케이션의 메모리 소비량을 계속 추적하고 이를 주 기준으로 삼는 것입니다. 위에서 설명한 툴을 통해 그래프가 시간에 따라 어떻게 변하는지, 특히 새 레벨을 로드한 후의 변화에 주의를 기울이십시오.
이에 대한 이유로는 여러 가지가 있습니다. 자세한 내용을 확인하려면 기기 로그를 살펴봐야 합니다. 기기를 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();
// Do not forget using UnityEngine.SceneManagement directive
SceneManager.LoadScene("Test");
}
현재 Type.GetProperty() 및 Type.GetValue() 는 .NET 2.0 Subset 프로파일에서만 지원됩니다. .NET API 호환성 레벨은 플레이어 설정에서 선택할 수 있습니다.
참고: Type.GetProperty() 및 Type.GetValue() 는 관리되는 코드 스트리핑과 호환되지 않을 수 있으며 제외되어야 할 수도 있습니다(이를 위해 스트리핑 프로세스 중에 직접 작성한 커스텀 스트립 불가 타입 리스트를 사용할 수도 있습니다). 자세한 내용은 iOS 플레이어 크기 최적화 가이드를 참조하십시오.
iOS의 Mono .NET 구현은 AOT(네이티브 코드로 사전 컴파일) 기술에 기반하는데, 여기에는 몇 가지 제한이 따릅니다. 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의 트램펄린을 추가로 할당하도록 알릴 수 있습니다. 추가 AOT 컴파일러 커맨드 라인 옵션은 플레이어 설정의 Other Settings 섹션에서 지정할 수 있습니다. 타입 0 트램펄린에는 ntrampolines=ABCD
를 지정하십시오. 여기서 ABCD는 필요한 새 트램펄린의 수(예: 4096)입니다. 타입 1 트램펄린에는nrgctx-trampolines=ABCD
를 지정하고 타입 2 트램펄린에는 nimt-trampolines=ABCD
를 지정하십시오.
최신 Xcode가 릴리스되면서 PNG 압축 및 최적화 툴에 몇 가지 변경 사항이 적용되었습니다. 이러한 변경으로 인해 스플래시 화면 수정을 위한 Unity iOS 런타임 검사에서 긍정 오류(false positive)가 발생할 수 있습니다. 이러한 문제가 발생하면 Unity를 공개적으로 이용 가능한 최신 버전으로 업그레이드하십시오. 그래도 문제가 지속되면 다음 해결 방법을 고려하십시오.
이 문제가 여전히 해결되지 않으면 Xcode에서 PNG 재압축을 비활성화할 수 있습니다.
가장 흔히 저지르는 실수는 WWW 다운로드가 항상 별도의 스레드에서 일어난다고 가정하는 것입니다. 일부 플랫폼에서는 맞을 수 있지만, 이를 당연한 사실로 받아들이면 안 됩니다. WWW 상태를 추적하는 가장 좋은 방법은 yield 문을 사용하거나 Update 메서드로 상태를 검사하는 것입니다. 이때 사용 중인 while 루프는 사용하지 마십시오.
UI에 대한 일부 작업을 수행하면 iOS가 창을 즉시 다시 그립니다. UIViewController가 있는 UIView를 메인 UIWindow에 추가하는 것이 가장 일반적인 예입니다. 스크립트에서 네이티브 함수를 호출하면 Unity의 PlayerLoop 내에서 발생하므로 PlayerLoop가 재귀적으로 호출됩니다. 이 경우 waitUntilDone 이 false로 설정된 performSelectorOnMainThread 메서드를 사용할 것을 고려해야 합니다. 그러면 Unity의 PlayerLoop 호출 간에 실행할 작업을 예약하도록 iOS에 알립니다.
애플리케이션이 에디터에서 정상적으로 동작하지만 iOS 프로젝트에 오류가 발생하는 경우 DLL(예: I18N.dll, I19N.West.dll) 누락이 그 원인일 수 있습니다. 이 경우 Unity.app 내에서 해당 dll을 복사하여 프로젝트의 Assets\Plugins 폴더에 붙여넣으십시오. Unity 앱 내 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 폴더로 옮기는 것입니다. 이 위치의 코드는 다른 어셈블리로 들어갑니다. 또한 특수 폴더 이름이 스크립트 컴파일에 영향을 미치는 방식에 관한 정보도 참조하십시오.