최신 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 호출을 안전하게 실행하지는 않습니다. 유일하게 로드 시간 오류를 호출 시간 오류로 연기합니다. 이 방법의 장점은 대체 구현을 사용하거나 사용자에게 앱의 기능을 기기에서 사용할 수 없다고 알리거나 코드 경로를 완전히 피함으로써 런타임 시 호출을 보호하고 정상적으로 대체할 수 있다는 것입니다.

Clang은 앱의 minSdkVersion에 사용할 수 없는 API를 보호되지 않은 호출로 호출할 때 경고 (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, *)에 의해 보호되어야 합니다. 빌드 오류가 없으면 minSdkVersion에서 API를 항상 사용할 수 있고 보호 기능이 필요하지 않거나, 빌드가 잘못 구성되어 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 검사는 사용되는 함수 범위에만 적용됩니다. Clang은 API 호출을 포함하는 함수가 보호된 범위 내에서만 호출되는 경우에도 경고를 표시합니다. 자체 코드에서 가드가 반복되지 않도록 하려면 API 가드 반복 방지를 참고하세요.

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

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

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

이 방법을 사용하는 것이 좋지만 문제를 감지하고 디버그하기가 더 어려워지므로 모르게 동작이 변경되는 것보다 이러한 위험을 고의로 수용해야 합니다.

주의사항

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

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

더 많은 개발자가 발생할 가능성이 높은 경우는 새 API가 포함된 라이브러리minSdkVersion보다 최신인 경우입니다. 이 기능은 약한 기호 참조만 사용 설정하며 약한 라이브러리 참조 같은 것은 없습니다. 예를 들어 minSdkVersion이 24라면 libvulkan.so를 연결하고 vkBindBufferMemory2에 보호된 호출을 실행할 수 있습니다. libvulkan.so를 API 24부터 기기에서 사용할 수 있기 때문입니다. 반면에 minSdkVersion이 23이었다면 dlopendlsym로 대체해야 합니다. API 23만 지원하는 기기의 기기에는 라이브러리가 없기 때문입니다. 이 문제를 해결하기 위한 좋은 해결 방법은 모르겠지만, 가능하면 새 API에서 새 라이브러리를 만드는 것을 더 이상 허용하지 않으므로 장기적으로 보면 저절로 해결될 것입니다.

도서관 저자용

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

공개 헤더에 이 동작이 필요한 경우 사용자 모두가 이 기능을 사용 설정해야 한다는 사실과 사용 시 위험성을 인지하도록 문서화해야 합니다.