최신 API 사용

이 페이지에서는 앱이 새 OS 버전에서 실행될 때 이전 기기와의 호환성을 유지하면서 새 OS 기능을 사용하는 방법을 설명합니다.

기본적으로 애플리케이션의 NDK API 참조는 강력한 참조입니다. Android의 동적 로더는 라이브러리가 로드될 때 이를 즉시 확인합니다. 기호를 찾을 수 없으면 앱이 중단됩니다. 이는 누락된 API가 호출될 때까지 예외가 발생하지 않는 Java의 동작과는 다릅니다.

따라서 NDK는 앱의 minSdkVersion보다 최신 버전인 API에 대한 강력한 참조를 만들지 못하도록 합니다. 이렇게 하면 테스트 중에 작동했지만 이전 기기에서 로드되지 않는(UnsatisfiedLinkErrorSystem.loadLibrary()에서 발생) 코드가 실수로 출시되는 것을 방지할 수 있습니다. 반면에 앱의 minSdkVersion보다 최신 API를 사용하는 코드를 작성하는 것은 더 어렵습니다. 일반적인 함수 호출이 아닌 dlopen()dlsym()를 사용하여 API를 호출해야 하기 때문입니다.

강력한 참조를 사용하는 대신 약한 참조를 사용할 수 있습니다. 라이브러리가 로드될 때 찾을 수 없는 약한 참조는 로드 실패가 아닌 해당 기호의 주소가 nullptr로 설정됩니다. 여전히 안전하게 호출할 수는 없지만 호출 사이트가 API를 사용할 수 없는 경우 API를 호출하지 못하도록 보호되는 한 나머지 코드는 실행할 수 있으며 dlopen()dlsym()를 사용하지 않고도 API를 정상적으로 호출할 수 있습니다.

약한 API 참조는 동적 링커의 추가 지원이 필요하지 않으므로 모든 버전의 Android에서 사용할 수 있습니다.

빌드에서 약한 API 참조 사용 설정

CMake

CMake를 실행할 때 -DANDROID_WEAK_API_DEFS=ON를 전달합니다. externalNativeBuild를 통해 CMake를 사용하는 경우 build.gradle.kts (또는 여전히 build.gradle를 사용하는 경우 Groovy 등가 항목)에 다음을 추가합니다.

android {
    // Other config...

    defaultConfig {
        // Other config...

        externalNativeBuild {
            cmake {
                arguments.add("-DANDROID_WEAK_API_DEFS=ON")
                // Other config...
            }
        }
    }
}

ndk-build

Application.mk 파일에 다음을 추가합니다.

APP_WEAK_API_DEFS := true

아직 Application.mk 파일이 없다면 Android.mk 파일과 동일한 디렉터리에 만듭니다. ndk-build에는 build.gradle.kts (또는 build.gradle) 파일을 추가로 변경할 필요가 없습니다.

기타 빌드 시스템

CMake 또는 ndk-build를 사용하지 않는 경우 빌드 시스템의 문서를 참고하여 이 기능을 사용 설정하는 권장 방법이 있는지 확인하세요. 빌드 시스템에서 이 옵션을 기본적으로 지원하지 않는 경우 컴파일 시 다음 플래그를 전달하여 이 기능을 사용 설정할 수 있습니다.

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

첫 번째는 약한 참조를 허용하도록 NDK 헤더를 구성합니다. 두 번째는 안전하지 않은 API 호출에 대한 경고를 오류로 전환합니다.

자세한 내용은 빌드 시스템 유지관리자 가이드를 참고하세요.

보호된 API 호출

이 기능은 새 API 호출을 자동으로 안전하게 만들지 않습니다. 로드 시간 오류를 호출 시간 오류로 지연시키는 작업만 실행합니다. 이 방법의 이점은 대체 구현을 사용하거나 사용자에게 앱의 해당 기능을 기기에서 사용할 수 없다고 알리거나 해당 코드 경로를 완전히 피하는 등 런타임에 해당 호출을 보호하고 적절하게 대체할 수 있다는 것입니다.

앱의 minSdkVersion에서 사용할 수 없는 API를 보호되지 않게 호출하면 Clang에서 경고 (unguarded-availability)를 내보낼 수 있습니다. ndk-build 또는 CMake 도구 모음 파일을 사용하는 경우 이 기능을 사용 설정하면 경고가 자동으로 사용 설정되고 오류로 승격됩니다.

다음은 이 기능을 사용 설정하지 않고 dlopen()dlsym()를 사용하여 API를 조건부로 사용하는 코드의 예입니다.

void LogImageDecoderResult(int result) {
    void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
    CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
    auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
        dlsym(lib, "AImageDecoder_resultToString")
    );
    if (func == nullptr) {
        LOG(INFO) << "cannot stringify result: " << result;
    } else {
        LOG(INFO) << func(result);
    }
}

읽기에는 다소 지저분하고 함수 이름이 중복됩니다 (C를 작성하는 경우 서명도 중복됨). 빌드는 성공적으로 이루어지지만 dlsym에 전달된 함수 이름을 실수로 잘못 입력하면 런타임 시 항상 대체가 사용됩니다. 모든 API에 이 패턴을 사용해야 합니다.

약한 API 참조를 사용하면 위의 함수를 다음과 같이 다시 작성할 수 있습니다.

void LogImageDecoderResult(int result) {
    if (__builtin_available(android 31, *)) {
        LOG(INFO) << AImageDecoder_resultToString(result);
    } else {
        LOG(INFO) << "cannot stringify result: " << result;
    }
}

내부적으로 __builtin_available(android 31, *)android_get_device_api_level()를 호출하고 결과를 캐시한 후 31(AImageDecoder_resultToString()를 도입한 API 수준)와 비교합니다.

__builtin_available에 사용할 값을 결정하는 가장 간단한 방법은 가드 (또는 __builtin_available(android 1, *)의 가드) 없이 빌드하려고 시도하고 오류 메시지에 표시된 내용을 실행하는 것입니다. 예를 들어 minSdkVersion 24를 사용하여 AImageDecoder_createFromAAsset()를 보호되지 않게 호출하면 다음이 생성됩니다.

error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]

이 경우 호출은 __builtin_available(android 30, *)로 보호되어야 합니다. 빌드 오류가 없으면 API가 항상 minSdkVersion에 사용할 수 있고 가드가 필요하지 않거나 빌드가 잘못 구성되어 unguarded-availability 경고가 사용 중지된 것입니다.

또는 NDK API 참조에서 각 API에 대해 'API 30에서 도입됨'과 같은 내용을 확인할 수 있습니다. 이 텍스트가 없으면 지원되는 모든 API 수준에서 API를 사용할 수 있다는 의미입니다.

API 가드 반복 방지

이를 사용하는 경우 앱에 최신 기기에서만 사용할 수 있는 코드 섹션이 있을 수 있습니다. 각 함수에서 __builtin_available() 검사를 반복하는 대신 특정 API 수준이 필요하다고 자체 코드에 주석을 달 수 있습니다. 예를 들어 ImageDecoder API 자체는 API 30에 추가되었으므로 이러한 API를 많이 사용하는 함수의 경우 다음과 같이 할 수 있습니다.

#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)

void DecodeImageWithImageDecoder() REQUIRES_API(30) {
    // Call any APIs that were introduced in API 30 or newer without guards.
}

void DecodeImageFallback() {
    // Pay the overhead to call the Java APIs via JNI, or use third-party image
    // decoding libraries.
}

void DecodeImage() {
    if (API_AT_LEAST(30)) {
        DecodeImageWithImageDecoder();
    } else {
        DecodeImageFallback();
    }
}

API 가드의 버그

Clang은 __builtin_available의 사용 방식에 매우 민감합니다. 리터럴(매크로로 대체될 수 있음) if (__builtin_available(...))만 작동합니다. if (!__builtin_available(...))와 같은 사소한 작업도 작동하지 않습니다 (Clang은 unguarded-availability뿐만 아니라 unsupported-availability-guard 경고도 내보냅니다). 향후 버전의 Clang에서 이 문제가 개선될 수 있습니다. 자세한 내용은 LLVM 문제 33161을 참고하세요.

unguarded-availability 검사는 사용되는 함수 범위에만 적용됩니다. API 호출이 포함된 함수가 보호된 범위 내에서만 호출되는 경우에도 Clang은 경고를 내보냅니다. 자체 코드에서 가드를 반복하지 않으려면 API 가드 반복 방지를 참고하세요.

기본값이 아닌 이유는 무엇인가요?

올바르게 사용되지 않으면 강력한 API 참조와 약한 API 참조의 차이점은 전자가 빠르고 명확하게 실패하는 반면 후자는 사용자가 누락된 API가 호출되도록 하는 작업을 실행할 때까지 실패하지 않는다는 것입니다. 이 경우 오류 메시지는 명확한 컴파일 시간 'AFoo_bar()를 사용할 수 없음' 오류가 아니라 segfault가 됩니다. 강력한 참조를 사용하면 오류 메시지가 훨씬 더 명확해지고 빠른 실패가 더 안전한 기본값이 됩니다.

이 기능은 새 기능이므로 이 동작을 안전하게 처리하도록 작성된 기존 코드는 거의 없습니다. Android를 염두에 두고 작성되지 않은 서드 파티 코드에는 항상 이 문제가 발생할 수 있으므로 현재 기본 동작을 변경할 계획은 없습니다.

이 방법을 사용하는 것이 좋습니다. 하지만 이 경우 문제를 감지하고 디버그하기가 더 어려워지므로 인지하지 못한 상태에서 동작이 변경되는 것보다 이러한 위험을 인지하고 수락하는 것이 좋습니다.

주의사항

이 기능은 대부분의 API에서 작동하지만 작동하지 않는 경우가 몇 가지 있습니다.

문제가 발생할 가능성이 가장 낮은 것은 최신 libc API입니다. 나머지 Android API와 달리 이러한 API는 __INTRODUCED_IN(X)뿐만 아니라 헤더에서 #if __ANDROID_API__ >= X로 보호되므로 약한 선언조차 표시되지 않습니다. 최신 NDK에서 지원하는 가장 오래된 API 수준은 r21이므로 가장 일반적으로 필요한 libc API는 이미 사용할 수 있습니다. 새로운 libc API는 출시마다 추가됩니다 (status.md 참고). 하지만 버전이 높을수록 개발자가 거의 필요하지 않은 특이 사례일 가능성이 높습니다. 하지만 이러한 개발자 중 한 명인 경우 minSdkVersion가 API보다 오래된 경우 지금은 계속 dlsym()를 사용하여 이러한 API를 호출해야 합니다. 이 문제는 해결할 수 있지만, 이렇게 하면 모든 앱의 소스 호환성이 손상될 위험이 있습니다 (libc API의 폴리필이 포함된 모든 코드는 libc 및 로컬 선언의 일치하지 않는 availability 속성으로 인해 컴파일되지 않음). 따라서 이 문제를 언제 해결할지 알 수 없습니다.

더 많은 개발자가 겪을 수 있는 경우는 새 API가 포함된 라이브러리minSdkVersion보다 최신 버전인 경우입니다. 이 기능은 약한 기호 참조만 사용 설정합니다. 약한 라이브러리 참조라는 것은 없습니다. 예를 들어 minSdkVersion가 24인 경우 libvulkan.so를 연결하고 vkBindBufferMemory2를 보호된 방식으로 호출할 수 있습니다. libvulkan.so는 API 24부터 기기에서 사용할 수 있기 때문입니다. 반면에 minSdkVersion가 23이면 API 23만 지원하는 기기에는 라이브러리가 기기에 없으므로 dlopendlsym로 대체해야 합니다. 이 케이스를 해결하기 위한 좋은 해결 방법을 알지 못하지만, 가능하면 더 이상 새 API가 새 라이브러리를 만들지 못하도록 허용하지 않으므로 장기적으로는 이 문제가 해결될 것입니다.

라이브러리 작성자

Android 애플리케이션에 사용할 라이브러리를 개발하는 경우 공개 헤더에서 이 기능을 사용하지 않는 것이 좋습니다. 이 기능은 오프라인 코드에서 안전하게 사용할 수 있지만 인라인 함수나 템플릿 정의와 같이 헤더의 코드에서 __builtin_available를 사용하는 경우 모든 소비자가 이 기능을 사용 설정하도록 강제합니다. NDK에서 이 기능을 기본적으로 사용 설정하지 않는 것과 같은 이유로 개발자는 소비자를 대신하여 이 선택을 하지 않아야 합니다.

공개 헤더에 이 동작이 필요한 경우 사용자에게 기능을 사용 설정해야 하며 이로 인해 발생할 수 있는 위험을 인지해야 한다고 문서화해야 합니다.