ART(Android 런타임)에서 앱 동작 확인

Android 런타임(ART)은 Android 5.0(API 레벨 21) 이상이 실행되는 기기의 기본 런타임입니다. 이 런타임은 Android 플랫폼과 앱의 성능과 유연성을 개선하는 수많은 기능을 제공합니다. ART의 새로운 기능에 대한 자세한 내용은 ART 소개를 참조하세요.

하지만 Dalvik에서 작동하는 일부 기법이 ART에서는 작동하지 않습니다. 이 문서에서는 ART와 호환되도록 기존 앱을 마이그레이션할 때 주의할 사항을 알려드립니다. ART와 실행할 때 대부분의 앱은 정상적으로 작동해야 합니다.

가비지 컬렉션(GC) 문제 해결

Dalvik에서는 가비지 컬렉션(GC)을 위해 앱에서 System.gc()를 명시적으로 호출하는 것이 유용한 경우가 많습니다. ART에서는 이 작업의 필요성이 훨씬 적으며, 특히 GC_FOR_ALLOC 유형의 발생을 예방하거나 단편화를 줄이기 위해 가비지 컬렉션을 호출하는 경우에는 더욱 불필요합니다. 어떤 런타임이 사용 중인지 확인하려면 System.getProperty("java.vm.version")을 호출합니다. ART가 사용 중이면, 속성값은 "2.0.0" 이상입니다.

또한, 메모리 관리를 개선하기 위해 간결한 가비지 컬렉터가 Android 오픈소스 프로젝트(AOSP)에서 개발 중입니다. 이 때문에 간결한 GC와 호환되지 않는 기술은 사용을 피해야 합니다(예: 객체 인스턴스 데이터에 포인터 저장). 이것은 JNI(Java Native Interface)를 사용하는 앱에서는 특히 더 중요합니다. 자세한 내용은 JNI 문제 예방을 참조하세요.

JNI 문제 예방

ART의 JNI는 Dalvik보다 약간 더 엄격합니다. 일반적인 문제를 잡아내기 위해 CheckJNI 모드를 사용하는 것은 매우 좋은 생각입니다. 앱에서 C/C++ 코드를 사용하는 경우, 다음 문서를 검토해야 합니다.

CheckJNI로 Android JNI 디버깅

가비지 컬렉션 문제를 위해 JNI 코드 확인

ART는 Android 오픈소스 프로젝트(AOSP)에서 간결한 가비지 컬렉터를 개발 중입니다. 간결한 가비지 컬렉터가 사용 중일 때만 메모리에서 객체를 이동할 수가 있습니다. C/C++ 코드를 사용하는 경우에는 간결한 GC와 호환되지 않는 작업을 수행하지 마세요. 저희는 몇 가지 잠재적인 문제를 파악하기 위해 CheckJNI의 성능을 개선했습니다(ICS에서 JNI 로컬 참조 변경에 설명).

특히 주목할 부분은 Get...ArrayElements()Release...ArrayElements() 함수의 사용입니다. 비간결 GC에서 Get...ArrayElements() 함수는 일반적으로 배열 객체 뒤의 실제 메모리에 대한 참조를 반환합니다. 반환된 배열 요소 중 하나를 변경하게 되면 배열 객체 자체가 변경됩니다(또한 Release...ArrayElements()의 인수는 대개 무시됩니다). 그러나 간결한 GC를 사용 중이면 Get...ArrayElements() 함수가 메모리 복사본을 반환할 수도 있습니다. 간결한 GC가 사용 중일 때 참조를 잘못 사용하게 되면 이로 인해 메모리 손상이나 기타 문제가 발생할 수 있습니다. 예:

  • 반환된 배열 요소를 변경하는 경우에는 완료 시에 적절한 Release...ArrayElements() 함수를 호출해야 합니다. 그래야만 여러분이 변경한 내용이 기본 배열 객체에 올바로 복사될 수 있습니다.
  • 메모리 배열 요소를 해제하는 경우에는 어떤 변경을 수행하는지에 따라 적절한 모드를 사용해야 합니다.
    • 배열 요소를 변경하지 않은 경우에는 JNI_ABORT 모드를 사용합니다. 이 모드에서는 변경한 내용을 기본 배열 객체에 복사하지 않고 메모리를 해제합니다.
    • 배열을 변경했지만 더 이상 참조가 필요 없는 경우에는 코드 0을 사용합니다. 이 코드는 배열 객체를 업데이트하고 메모리 복사본을 해제합니다.
    • 커밋하려는 배열을 변경했지만 배열 복사본을 유지하려는 경우에는 JNI_COMMIT을 사용해야 합니다. 이 코드는 기본 배열 객체를 업데이트하고 복사본을 유지합니다.
  • Release...ArrayElements()를 호출하면 원래 Get...ArrayElements()에서 반환했던 것과 동일한 포인터가 반환됩니다. 예를 들어 반환된 배열 요소를 검색하기 위해 원래 포인터를 증가시킨 다음, 이 증가된 포인터를 Release...ArrayElements()에 전달하는 것은 안전한 방법이 아닙니다. 수정된 포인터를 전달할 경우 메모리가 잘못 해제될 수 있고 이로 인해 메모리가 손상될 수 있습니다.

오류 처리

ART의 JNI는 Dalvik에서는 발생하지 않는 여러 가지 경우의 오류를 유발합니다. (위에서 말한 것처럼 CheckJNI로 테스트를 수행하면 이러한 경우의 오류를 많이 잡아낼 수 있습니다.)

예를 들어 ProGuard와 같은 도구에 의해 제거되어서 더 이상 존재하지 않는 메서드와 함께 RegisterNatives가 호출된 경우, 이제 ART에서 NoSuchMethodError가 적절히 발생합니다.

08-12 17:09:41.082 13823 13823 E AndroidRuntime: FATAL EXCEPTION: main
08-12 17:09:41.082 13823 13823 E AndroidRuntime: java.lang.NoSuchMethodError:
    no static or non-static method
    "Lcom/foo/Bar;.native_frob(Ljava/lang/String;)I"
08-12 17:09:41.082 13823 13823 E AndroidRuntime:
    at java.lang.Runtime.nativeLoad(Native Method)
08-12 17:09:41.082 13823 13823 E AndroidRuntime:
    at java.lang.Runtime.doLoad(Runtime.java:421)
08-12 17:09:41.082 13823 13823 E AndroidRuntime:
    at java.lang.Runtime.loadLibrary(Runtime.java:362)
08-12 17:09:41.082 13823 13823 E AndroidRuntime:
    at java.lang.System.loadLibrary(System.java:526)

또한 메서드 없이 RegisterNatives가 호출된 경우 ART에서 오류가 기록됩니다(logcat에서 볼 수 있음).

W/art     ( 1234): JNI RegisterNativeMethods: attempt to register 0 native
methods for <classname>

또한 JNI 함수인 GetFieldID()GetStaticFieldID()에서 단순히 null이 반환되는 대신 이제 NoSuchFieldError가 적절히 발생합니다. 마찬가지로 이제 GetMethodID()GetStaticMethodID()에서 NoSuchMethodError가 적절히 발생합니다. 이 경우 처리되지 않은 예외(또는 네이티브 코드의 Java 호출자에 발생한 예외)로 인해 CheckJNI 실패가 발생할 수 있습니다. 이것은 CheckJNI 모드에서 ART 호환 앱을 테스트할 때 특히 중요합니다.

JNI 사양에 따라 ART에서는 JNI CallNonvirtual...Method() 메서드(예: CallNonvirtualVoidMethod()) 사용자들이 하위 클래스를 사용하는 대신 메서드 선언 클래스를 사용해야 합니다.

스택 크기 문제 예방

Dalvik에서는 네이티브 코드와 Java 코드에 별도 스택을 사용하며, 기본 Java 스택 크기는 32KB이고 기본 네이티브 스택 크기는 1MB입니다. 더 나은 지역성(locality)을 위해 ART에는 통합 스택이 있습니다. 일반적으로 ART Thread 스택 크기는 거의 Dalvik과 동일해야 합니다. 그러나 스택 크기를 명시적으로 설정하는 경우에는 ART에서 실행 중인 앱에 대해 이 값을 다시 설정할 필요가 있습니다.

  • Java의 경우, 명시적 스택 크기를 지정하는 Thread 생성자에 대한 호출을 검토해 보세요. 예를 들어 StackOverflowError가 발생하는 경우 크기를 늘릴 필요가 있습니다.
  • C/C++의 경우, JNI를 통해 Java 코드를 실행하는 스레드에 대해 pthread_attr_setstack()pthread_attr_setstacksize() 사용을 검토해 보세요. 다음은 pthread 크기가 너무 작을 때 JNI AttachCurrentThread()를 호출하려고 시도하면 기록되는 오류의 예시입니다.
    F/art: art/runtime/thread.cc:435]
        Attempt to attach a thread with a too-small stack (16384 bytes)

객체 모델 변경

Dalvik에서는 하위 클래스가 package-private 메서드를 재정의하도록 잘못 허용했습니다. 이런 경우 ART에서는 경고가 발생합니다.

Before Android 4.1, method void com.foo.Bar.quux()
would have incorrectly overridden the package-private method in
com.quux.Quux

다른 패키지에서 클래스 메서드를 재정의하려는 경우에는 해당 메서드를 public 또는 protected로 선언하세요.

이제 Object에는 private 필드가 있습니다. 클래스 계층 구조로 필드에 반영되는 앱은 Object의 필드를 조회하려고 시도해서는 안 됩니다. 예를 들어 직렬화 프레임워크의 일환으로 클래스계층 구조를 반복 중인 경우, 다음 위치에서 중단하고

Class.getSuperclass() == java.lang.Object.class

메서드가 null을 반환할 때까지 계속해서는 안 됩니다.

인수가 없는 경우 이제 프록시 InvocationHandler.invoke()는 빈 배열을 수신하는 대신 null을 수신합니다. 이 동작은 이전에 Dalvik에서 기록되었지만 올바로 처리되지는 않았습니다. 이전 버전의 Mockito는 이와 관련해 문제가 있으므로, ART로 테스트할 때는 업데이트된 버전의 Mockito를 사용하세요.

AOT 컴파일 문제 해결

ART의 AOT(Ahead-Of-Time) Java 컴파일은 모든 표준 Java 코드에서 작동해야 합니다. 컴파일은 ART의 dex2oat 도구로 수행됩니다. 설치 시에 dex2oat 관련 문제가 발생하는 경우 저희에게 알려주시면(문제 보고 참조) 최대한 빨리 문제를 수정하겠습니다. 주목할 몇 가지 문제:

  • ART는 설치 시에 Dalvik보다 더 엄격한 바이트코드 검사를 수행합니다. Android 빌드 도구에서 생성된 코드는 괜찮을 것입니다. 그러나 일부 후처리 도구(특히 난독화를 수행하는 도구)에서 생성될 수 있는 잘못된 파일들은 Dalvik에서는 허용되지만 ART에서는 거부됩니다. 저희는 이러한 문제를 찾아서 해결하기 위해 도구 공급업체와 협력하고 있습니다. 많은 경우 최신 버전의 도구를 구하고 DEX 파일을 재생성함으로써 이러한 문제를 해결할 수 있습니다.
  • ART 확인자에 의해 플래그되는 일반적인 문제는 다음과 같습니다.
    • 잘못된 제어 흐름
    • 불균형인 moniterenter/moniterexit
    • 길이가 0인 매개변수 형식 목록 크기
  • 일부 앱의 경우 /system/framework, /data/dalvik-cache 또는 DexClassLoader의 최적화된 출력 디렉토리에서 설치된 .odex 파일 형식에 종속성이 있습니다. 이제 이들 파일은 ELF 파일이 되었고 확장된 형식의 DEX 파일이 아닙니다. ART는 Dalvik과 동일한 명명 규칙과 잠금 규칙을 따르려고 노력하지만, 앱은 파일 형식에 종속되어서는 안 되며 이 형식이 예고 없이 변경될 수 있습니다.

    참고: Android 8.0(API 레벨 26) 이상에서 DexClassLoader에 최적화된 출력 디렉토리의 사용이 중단되었습니다. 자세한 내용은 DexClassLoader() 생성자를 참조하세요.

문제 보고

앱 JNI 문제가 아닌 다른 문제가 발생한 경우, Android 오픈소스 프로젝트(AOSP)의 Issue Tracker를 통해 문제를 보고해 주세요(https://code.google.com/p/android/issues/list). "adb bugreport"와 Google Play 스토어의 앱 링크를 포함하세요(있는 경우). 그렇지 않은 경우 문제를 재현하는 APK를 첨부해 주세요. 참고로 이러한 문제(첨부 포함)는 누구나 볼 수 있습니다.