使用新版 API

本頁面說明應用程式在新的 OS 版本上執行時,如何使用新的 OS 功能,同時維持與舊裝置的相容性。

根據預設,應用程式中對 NDK API 的參照為強參照。載入程式庫時,Android 的動態載入器會立即解析這些類別。如果找不到符號,應用程式就會中止。這與 Java 的行為相反,在 Java 中,除非呼叫缺少的 API,否則不會擲回例外狀況。

因此,NDK 會禁止您建立比應用程式 minSdkVersion 更新的 API 的強式參照。這樣一來,您就不會在舊版裝置上,不小心發布在測試期間運作,但無法載入的程式碼 (UnsatisfiedLinkError 會從 System.loadLibrary() 擲回)。另一方面,如果您要編寫使用比應用程式 minSdkVersion 更新的 API 的程式碼,難度會更高,因為您必須使用 dlopen()dlsym() 呼叫 API,而非使用一般函式呼叫。

使用強式參照的替代方案是使用弱參照。在載入程式庫時找不到的弱參照會導致該符號的位址設為 nullptr,而非載入失敗。這些 API 仍無法安全呼叫,但只要呼叫端受到保護,可避免在 API 無法使用時呼叫 API,其他程式碼就能正常執行,您也可以正常呼叫 API,而無需使用 dlopen()dlsym()

弱 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 24AImageDecoder_createFromAAsset() 進行未受保護的呼叫,會產生以下結果:

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

在這種情況下,呼叫應由 __builtin_available(android 30, *) 保護。如果沒有任何建構錯誤,表示 minSdkVersion 一律能使用,且不需要防護,或者您的建構作業設定錯誤而停用 unguarded-availability 警告。

或者,NDK API 參考資料會針對每個 API 說明「Introduced in API 30」(在 API 30 中推出)。如果沒有這段文字,表示該 API 適用於所有支援的 API 級別。

避免重複使用 API 防護機制

如果您使用此功能,應用程式中可能會有部分程式碼只能在較新的裝置上使用。您可以標註自己的程式碼,指出需要特定 API 級別,而非在每個函式中重複執行 __builtin_available() 檢查。舉例來說,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 會發出 unsupported-availability-guard 警告,以及 unguarded-availability)。這項功能可能會在未來的 Clang 版本中改善。詳情請參閱 LLVM 問題 33161

unguarded-availability 的檢查作業只會套用至使用這些函式的函式範圍。即使含有 API 呼叫的函式只會從受保護的範圍內呼叫,Clang 也會發出警告。如要避免在程式碼中重複使用守衛,請參閱「避免重複使用 API 守衛」。

為什麼不是預設值?

除非使用正確,否則強式 API 參照和弱式 API 參照的差異在於,前者會快速且明顯地失敗,而後者必須等到使用者採取導致呼叫缺少 API 的動作時才會失敗。發生這種情況時,錯誤訊息不會是清楚的編譯時間「AFoo_bar() 不可用」錯誤,而是會是分段錯誤。使用強式參照,錯誤訊息會更清楚,且快速失敗是更安全的預設值。

由於這是新功能,因此很少有現有程式碼可安全地處理這項行為。未以 Android 編寫的第三方程式碼很可能都會發生這個問題,因此目前預設行為目前沒有變更。

我們建議您使用此選項,但由於這會使問題更難偵測和偵錯,因此您應明確接受這些風險,而不是讓行為在您不知情的情況下發生變化。

注意事項

這項功能適用於大多數 API,但在少數情況下無法運作。

較新的 libc API 最不可能發生問題。與其他 Android API 不同的是,這些 API 會在標頭中使用 #if __ANDROID_API__ >= X 進行防護,而非僅使用 __INTRODUCED_IN(X),這樣一來,即使是弱式宣告也無法顯示。由於最舊的 API 級別現代 NDK 支援為 r21,因此現在已提供最常見的 libc API。每個版本都會新增新的 libc API (請參閱 status.md),但越新的 API 越有可能是少數開發人員才會需要的特殊情況。也就是說,如果您是這類開發人員,且 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,但如果您在標頭的任何程式碼中 (例如內嵌函式或範本定義) 依賴 __builtin_available,就會強制所有使用者啟用這項功能。基於相同原因,我們不會在 NDK 中預設啟用這項功能,因此請避免代替消費者做出這項選擇。

如果您確實需要在公開標頭中使用這項行為,請務必在文件中說明,讓使用者知道他們需要啟用這項功能,並瞭解相關風險。