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

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

하지만 Dalvik에서 작동하는 일부 기법이 ART에서는 작동하지 않습니다. 이 문서에서는 ART와 호환되도록 기존 앱을 이전할 때 유의해야 할 사항을 안내합니다. ART로 실행할 때 대부분의 앱은 정상적으로 작동합니다.

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

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

ART는 동시에 Java 힙을 압축하는 동시 복사 (CC) 컬렉터를 사용합니다. 따라서 간결한 GC와 호환되지 않는 기법 (예: 객체 인스턴스 데이터에 포인터 저장)은 사용하지 않아야 합니다. 이는 자바 네이티브 인터페이스 (JNI)를 사용하는 앱에 특히 중요합니다. 자세한 내용은 JNI 문제 예방을 참고하세요.

JNI 문제 예방

ART의 JNI는 Dalvik의 JNI보다 다소 엄격합니다. 일반적인 문제를 포착하려면 CheckJNI 모드를 사용하는 것이 좋습니다. 앱에서 C/C++ 코드를 사용하는 경우 다음 도움말을 검토해야 합니다.

CheckJNI로 Android JNI 디버깅

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

동시 복사 (CC) 수집기는 압축을 위해 메모리에서 객체를 이동할 수 있습니다. C/C++ 코드를 사용하는 경우에는 간결한 GC와 호환되지 않는 작업을 실행하지 마세요. Google에서는 몇 가지 잠재적인 문제를 식별하기 위해 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에서 발생하지 않는 여러 경우에 ART의 JNI에서 오류가 발생합니다. (다시 한 번 언급하지만 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 호환 앱을 테스트하는 것이 특히 중요합니다.

ART에서는 JNI CallNonvirtual...Method() 메서드(예: CallNonvirtualVoidMethod()) 사용자가 JNI 사양에 따라 서브클래스가 아닌 메서드의 선언 클래스를 사용해야 합니다.

스택 크기 문제 예방

Dalvik은 네이티브 코드와 자바 코드를 위한 별도의 스택을 보유하고 있으며, 기본 자바 스택 크기는 32KB이고 기본 네이티브 스택 크기는 1MB입니다. ART에는 더 나은 지역성을 위한 통합 스택이 있습니다. 일반적으로 ART Thread 스택 크기는 거의 Dalvik과 동일해야 합니다. 그러나 스택 크기를 명시적으로 설정하는 경우에는 ART에서 실행되는 앱의 값을 다시 설정해야 할 수도 있습니다.

  • 자바에서는 명시적 스택 크기를 지정하는 Thread 생성자 호출을 검토합니다. 예를 들어 StackOverflowError가 발생하면 크기를 늘려야 합니다.
  • C/C++에서는 JNI를 통해 자바 코드도 실행하는 스레드에 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에 비공개 필드가 있습니다. 클래스 계층 구조의 필드에 반영되는 앱은 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와 관련된 문제가 발생하면 가능한 한 빨리 해결할 수 있도록 Google에 알려주세요 (문제 신고 참고). 주목할 몇 가지 문제:

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

    참고: Android 8.0 (API 수준 26) 이상에서 DexClassLoader에 최적화된 출력 디렉터리는 지원 중단되었습니다. 자세한 내용은 DexClassLoader() 생성자에 관한 문서를 참고하세요.

문제 보고하기

앱 JNI 문제 이외의 원인으로 문제가 발생한 경우 Android 오픈소스 프로젝트 Issue Tracker(https://code.google.com/p/android/issues/list)를 통해 신고해 주세요. 가능한 경우 "adb bugreport" 및 Google Play 스토어의 앱으로 연결되는 링크를 제공해 주세요. 또는 가능한 경우 문제를 재현하는 APK를 첨부해 주세요. 이 문제 (첨부파일 포함)는 모든 사용자에게 표시됩니다.