本頁面說明應用程式在新的 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 24
對 AImageDecoder_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,您必須改用 dlopen
和 dlsym
,因為在只支援 API 23 的裝置上,程式庫不會存在於裝置上。我們不清楚如何解決這個問題,但從長遠來看,這個問題會自行解決,因為我們 (盡可能) 不再允許新 API 建立新程式庫。
圖書館作者
如果您要開發可用於 Android 應用程式的程式庫,請避免在公開標頭中使用這項功能。您可以在離線程式碼中安全使用 __builtin_available
,但如果您在標頭的任何程式碼中 (例如內嵌函式或範本定義) 依賴 __builtin_available
,就會強制所有使用者啟用這項功能。基於相同原因,我們不會在 NDK 中預設啟用這項功能,因此請避免代替消費者做出這項選擇。
如果您確實需要在公開標頭中使用這項行為,請務必在文件中說明,讓使用者知道他們需要啟用這項功能,並瞭解相關風險。