本頁面說明應用程式如何在執行新作業系統版本時使用新功能,同時維持與舊版裝置的相容性。
根據預設,應用程式中對 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, *)
保護。如果沒有建構錯誤,表示 API 一律可供 minSdkVersion
使用,因此不需要防護機制;或是建構作業設定錯誤,因此 unguarded-availability
警告已停用。
或者,NDK API 參考資料會針對每個 API 說明「在 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,但在某些情況下無法使用。
在 NDK r28 之前,這個做法不適用於 libc 或 libm API。
較多開發人員可能會遇到的情況是,包含新 API 的程式庫比您的 minSdkVersion
更新。這項功能只會啟用弱符號參照項目,並沒有弱程式庫參照項目。舉例來說,如果 minSdkVersion
為 24,您可以連結 libvulkan.so
,並對 vkBindBufferMemory2
進行受保護的呼叫,因為 libvulkan.so
適用於 API 24 以上的裝置。另一方面,如果 minSdkVersion
為 23,您必須改用 dlopen
和 dlsym
,因為在僅支援 API 23 的裝置上,程式庫不會存在於裝置上。我們不清楚如何解決這個問題,但從長遠來看,這個問題會自行解決,因為我們 (盡可能) 不再允許新 API 建立新程式庫。
適用於程式庫作者
如果您要開發可用於 Android 應用程式的程式庫,請避免在公開標頭中使用這項功能。您可以在離線程式碼中安全使用 __builtin_available
,但如果您在標頭的任何程式碼中 (例如內嵌函式或範本定義) 依賴 __builtin_available
,就會強制所有使用者啟用這項功能。基於相同原因,我們不會在 NDK 中預設啟用這項功能,因此請避免代替消費者做出這項選擇。
如果您確實需要在公開標頭中使用這項行為,請務必在文件中說明,讓使用者知道他們需要啟用這項功能,並瞭解這麼做的風險。