使用新版 API

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

根據預設,應用程式中的 NDK API 參照是強力參照。Android 的動態載入器會在載入程式庫時積極解決這些錯誤。如果找不到這些符號,應用程式會取消。這與 Java 的行為相反,在呼叫缺少的 API 之前,才會擲回例外狀況。

因此,NDK 會禁止您建立比應用程式 minSdkVersion 新更新的 API 的強式參照。這麼做可防止在測試期間意外運送程式碼,但無法在舊版裝置上載入 (從 System.loadLibrary() 擲回 UnsatisfiedLinkError)。另一方面,相較於應用程式 minSdkVersion 的新 API,撰寫程式碼使用的 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。只有將載入時間錯誤延後到呼叫時間錯誤。這種做法的好處是,無論是使用替代實作項目,還是通知使用者裝置無法使用應用程式功能,或完全避開該程式碼路徑,都可以確保在執行階段呼叫並優雅地退場。

當您呼叫未受保護的 API 時,如果 API 不適用於應用程式的 minSdkVersion,Clang 便會發出警告 (unguarded-availability)。如果您使用 ndk-build 或我們的 CMake 工具鍊檔案,系統會在啟用這項功能時將警告自動提升為錯誤。

-Werror=unguarded-availability

以下範例程式碼使用 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 的「在 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 會發出 unsupported-availability-guard 警告,以及 unguarded-availability)。這可能會在日後的 Clang 版本中改善。詳情請參閱 LLVM 問題 33161

檢查 unguarded-availability 僅適用於使用該函式的函式範圍。即使含有 API 呼叫的函式僅在受到保護的範圍內呼叫,Clang 仍會發出警示。如要避免在您自己的程式碼中重複防衛,請參閱「避免重複 API 防護」。

為什麼這不是預設值?

除非正確使用,否則強式 API 參照和弱 API 參照之間的差別在於前者會快速且明顯地失敗,而後者則要等到使用者採取動作導致系統呼叫缺少的 API 後才會失敗。在這種情況下,錯誤訊息不會是編譯時間「AFoo_bar() is not available」的明確錯誤,而是分段錯誤。使用強而有力的參照,錯誤訊息可更清楚地顯示,而「失敗速度」設定是更加安全的預設值。

由於這是新功能,因此只需編寫少量現有程式碼,即可安全地處理這個行為。未以 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 中預設啟用這項功能,請避免代表消費者進行選擇。

如果您必須在公開標頭中執行這項操作,請務必讓使用者瞭解需要啟用該功能,並瞭解這麼做的風險。