Trang này giải thích cách ứng dụng của bạn có thể sử dụng chức năng mới của hệ điều hành khi chạy trên Các phiên bản hệ điều hành mà vẫn duy trì khả năng tương thích với các thiết bị cũ.
Theo mặc định, các thông tin tham chiếu đến API NDK trong ứng dụng của bạn sẽ là thông tin tham chiếu mạnh mẽ. Trình tải động của Android sẽ hăng say giải quyết các vấn đề này khi thư viện của bạn đã tải xong. Nếu không tìm thấy ký hiệu, ứng dụng sẽ huỷ. Điều này trái với cách Java hoạt động, trong đó một ngoại lệ sẽ không được gửi cho đến khi API bị thiếu được có tên.
Vì lý do này, NDK sẽ ngăn bạn tạo các tệp đối chiếu rõ ràng đến
Các API mới hơn minSdkVersion
của ứng dụng. Việc này giúp bảo vệ bạn khỏi
vô tình gửi mã đã hoạt động trong quá trình thử nghiệm nhưng không tải được
(UnsatisfiedLinkError
sẽ được gửi từ System.loadLibrary()
) vào các phiên bản cũ hơn
thiết bị. Mặt khác, sẽ khó viết mã hơn sử dụng API
mới hơn minSdkVersion
của ứng dụng, vì bạn phải gọi các API bằng cách sử dụng
dlopen()
và dlsym()
thay vì lệnh gọi hàm thông thường.
Lựa chọn thay thế cho việc sử dụng tham chiếu mạnh là sử dụng tham chiếu yếu. Yếu
tham chiếu không được tìm thấy khi thư viện tải kết quả trong địa chỉ của
biểu tượng đó được đặt thành nullptr
thay vì không tải được. Họ vẫn
không thể được gọi một cách an toàn, nhưng miễn là các vị trí gọi được bảo vệ để ngăn chặn cuộc gọi
khi không có API, bạn có thể chạy phần còn lại của mã và bạn có thể
gọi API như bình thường mà không cần sử dụng dlopen()
và dlsym()
.
Các tệp tham chiếu API yếu không cần trình liên kết động hỗ trợ thêm, để có thể sử dụng với mọi phiên bản Android.
Bật các tệp tham chiếu API yếu trong bản dựng
CMake
Truyền -DANDROID_WEAK_API_DEFS=ON
khi chạy CMake. Nếu bạn đang sử dụng CMake qua
externalNativeBuild
, hãy thêm đoạn mã sau vào build.gradle.kts
(hoặc
Groovy tương đương nếu bạn vẫn đang sử dụng build.gradle
):
android {
// Other config...
defaultConfig {
// Other config...
externalNativeBuild {
cmake {
arguments.add("-DANDROID_WEAK_API_DEFS=ON")
// Other config...
}
}
}
}
ndk-build
Thêm đoạn mã sau vào tệp Application.mk
:
APP_WEAK_API_DEFS := true
Nếu bạn chưa có tệp Application.mk
, hãy tạo tệp này trong cùng một tệp
làm tệp Android.mk
. Các thay đổi bổ sung đối với
ndk-build không cần tệp build.gradle.kts
(hoặc build.gradle
).
Các hệ thống xây dựng khác
Nếu bạn không dùng CMake hoặc ndk-build, hãy tham khảo tài liệu dành cho bản dựng hệ thống để xem có cách nào được đề xuất để bật tính năng này không. Nếu bản dựng của bạn hệ thống không hỗ trợ sẵn tùy chọn này, bạn có thể bật tính năng này bằng cách truyền các cờ sau khi biên dịch:
-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability
Đầu tiên, định cấu hình các tiêu đề NDK để cho phép các tệp tham chiếu yếu. Lượt thứ hai cảnh báo về lỗi gọi API không an toàn.
Xem Hướng dẫn bảo trì hệ thống xây dựng để biết thêm thông tin.
Lệnh gọi API được bảo vệ
Tính năng này không tự động thực hiện các lệnh gọi đến API mới. Điều duy nhất có là trì hoãn lỗi thời gian tải thành lỗi thời gian gọi. Lợi ích là bạn có thể bảo vệ lệnh gọi đó trong thời gian chạy và quay lại dễ dàng, cho dù bằng cách sử dụng cách triển khai thay thế hoặc thông báo cho người dùng rằng tính năng đó của ứng dụng không có trên thiết bị của họ hoặc tránh hoàn toàn đường dẫn mã đó.
Clang có thể phát ra cảnh báo (unguarded-availability
) khi bạn tạo trạng thái không được bảo vệ
lệnh gọi đến một API không dùng được cho minSdkVersion
của ứng dụng. Nếu bạn
bằng bản dựng ndk hoặc tệp chuỗi công cụ CMake, thì cảnh báo đó sẽ được tự động
đã bật và quảng bá lên lỗi khi bật tính năng này.
Dưới đây là ví dụ về một số mã sử dụng API có điều kiện mà không cần
đã bật tính năng này, bằng cách sử dụng dlopen()
và dlsym()
:
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);
}
}
Hơi rối khi đọc, tên hàm bị trùng lặp (và nếu
bạn đang viết C cũng như chữ ký), nhưng công cụ sẽ được tạo
lấy tính năng dự phòng trong thời gian chạy nếu bạn vô tình đánh máy sai tên hàm đã chuyển
vào dlsym
, đồng thời bạn phải sử dụng mẫu này cho mọi API.
Với các tham chiếu API yếu, hàm trên có thể được viết lại thành:
void LogImageDecoderResult(int result) {
if (__builtin_available(android 31, *)) {
LOG(INFO) << AImageDecoder_resultToString(result);
} else {
LOG(INFO) << "cannot stringify result: " << result;
}
}
Tính năng nâng cao, __builtin_available(android 31, *)
cuộc gọi
android_get_device_api_level()
, lưu kết quả vào bộ nhớ đệm rồi so sánh với 31
(là cấp độ API đã ra mắt AImageDecoder_resultToString()
).
Cách đơn giản nhất để xác định giá trị cần sử dụng cho __builtin_available
là
cố gắng xây dựng mà không có người bảo vệ (hoặc người bảo vệ của
__builtin_available(android 1, *)
) rồi làm theo thông báo lỗi.
Ví dụ: một lệnh gọi không được bảo vệ đến AImageDecoder_createFromAAsset()
với
minSdkVersion 24
sẽ cho ra:
error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]
Trong trường hợp này, cuộc gọi phải được bảo vệ bằng __builtin_available(android 30, *)
.
Nếu không có lỗi bản dựng, thì API luôn có sẵn để
minSdkVersion
và không cần trình bảo vệ hoặc bản dựng của bạn bị định cấu hình sai và
Cảnh báo unguarded-availability
đã bị tắt.
Ngoài ra, tài liệu tham khảo API NDK sẽ cho biết nội dung nào đó dọc theo các dòng "Ra mắt trong API 30" cho từng API. Nếu không có văn bản đó, điều đó có nghĩa là API có sẵn cho tất cả cấp độ API được hỗ trợ.
Tránh lặp lại các biện pháp bảo vệ API
Nếu đang sử dụng cách này, có thể bạn sẽ có một số phần mã trong ứng dụng
chỉ sử dụng được trên các thiết bị đủ mới. Thay vì lặp lại
__builtin_available()
kiểm tra từng hàm, bạn có thể chú thích
riêng mã yêu cầu một cấp độ API nhất định. Ví dụ: ImageDecoder API
chúng được thêm vào API 30, vì vậy, đối với các hàm tận dụng nhiều
Bạn có thể làm những việc như:
#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();
}
}
Các nút lệnh của trình bảo vệ API
Clang rất cụ thể về cách sử dụng __builtin_available
. Chỉ giá trị cố định
(mặc dù có thể bị thay thế vĩ mô) if (__builtin_available(...))
hoạt động. Đồng đều
các thao tác đơn giản như if (!__builtin_available(...))
sẽ không hoạt động (Clang
sẽ phát ra cảnh báo unsupported-availability-guard
, cũng như
unguarded-availability
). Tính năng này có thể được cải thiện trong một phiên bản Clang sau này. Xem
Vấn đề LLVM 33161 để biết thêm thông tin.
Các bước kiểm tra cho unguarded-availability
chỉ áp dụng cho phạm vi hàm mà tại đó
thường được sử dụng. Clang sẽ phát ra cảnh báo ngay cả khi hàm có lệnh gọi API là
chỉ được gọi từ trong phạm vi được bảo vệ. Để tránh lặp lại các vệ sĩ trong
mã của riêng bạn, xem phần Tránh lặp lại các biện pháp bảo vệ API.
Tại sao đây không phải là chế độ mặc định?
Trừ phi được sử dụng đúng cách, sự khác biệt giữa tệp tham chiếu API mạnh và API yếu cho rằng quy tắc đầu tiên sẽ thất bại nhanh chóng và rõ ràng, trong khi API này sẽ không bị lỗi cho đến khi người dùng thực hiện hành động gây ra API bị thiếu để được gọi. Khi điều này xảy ra, thông báo lỗi sẽ không rõ ràng thời gian biên dịch "AFoo_bar() không khả dụng" lỗi, thì đó sẽ là lỗi đơn lẻ. Bằng tham chiếu mạnh, thông báo lỗi rõ ràng hơn nhiều và không thành công nhanh là mặc định an toàn hơn.
Vì đây là tính năng mới nên rất ít mã hiện có được viết để xử lý hành vi này một cách an toàn. Mã của bên thứ ba không được viết riêng cho Android có thể sẽ luôn gặp phải sự cố này, vì vậy, hiện chưa có kế hoạch cho hành vi mặc định luôn thay đổi.
Bạn nên sử dụng tuỳ chọn này, nhưng vì thao tác này sẽ gây nhiều vấn đề hơn khó có thể phát hiện và gỡ lỗi, bạn nên cẩn trọng chấp nhận những rủi ro đó thay vì hành vi mà bạn không biết.
Chú ý
Tính năng này hoạt động trên hầu hết các API, nhưng có một vài trường hợp tính năng này không hoạt động cơ quan.
Ít có khả năng xảy ra sự cố nhất là các API libc mới hơn. Không giống như phần còn lại của
API Android, những API đó được bảo vệ bằng #if __ANDROID_API__ >= X
trong tiêu đề
chứ không chỉ __INTRODUCED_IN(X)
, điều này ngăn chặn cả việc khai báo yếu
đang được xem. Vì khả năng hỗ trợ NDK hiện đại cấp độ API cũ nhất là r21, nên
libc API thường cần thiết đã có sẵn. Mỗi API libc mới được thêm vào
phát hành (xem status.md), nhưng chúng càng mới thì càng có nhiều khả năng
là một tình huống đặc biệt mà ít nhà phát triển sẽ cần đến. Tuy nhiên, nếu bạn là một trong số
những nhà phát triển đó, bây giờ bạn cần tiếp tục sử dụng dlsym()
để gọi những nhà phát triển đó
các API nếu minSdkVersion
của bạn cũ hơn API. Đây là một vấn đề có thể giải quyết được,
nhưng làm như vậy có nguy cơ phá vỡ khả năng tương thích nguồn cho tất cả ứng dụng (bất kỳ
mã có chứa polyfill của API libc sẽ không thể biên dịch do
thuộc tính availability
không khớp trên khai báo libc và cục bộ), vì vậy
chúng tôi không chắc liệu chúng tôi có khắc phục được vấn đề đó hay không và khi nào.
Vấn đề mà nhiều nhà phát triển có thể gặp phải là khi thư viện
chứa API mới mới hơn minSdkVersion
. Chỉ tính năng này
cho phép tham chiếu ký hiệu yếu; không có thứ gì là thư viện yếu
tham chiếu. Ví dụ: nếu minSdkVersion
của bạn là 24, bạn có thể liên kết
libvulkan.so
và thực hiện cuộc gọi có bảo vệ đến vkBindBufferMemory2
vì
libvulkan.so
có sẵn trên các thiết bị kể từ API 24. Mặt khác,
nếu minSdkVersion
của bạn là 23, bạn phải quay lại dùng dlopen
và dlsym
vì thư viện sẽ không tồn tại trên thiết bị chỉ hỗ trợ
API 23. Chúng tôi không biết giải pháp tốt để khắc phục trường hợp này, nhưng về lâu dài
thì từ khoá đó sẽ tự khắc phục được vì chúng tôi (bất cứ khi nào có thể) không còn cho phép
API để tạo thư viện mới.
Dành cho tác giả thư viện
Nếu bạn đang phát triển một thư viện để dùng trong các ứng dụng Android, bạn nên
hãy tránh sử dụng tính năng này trong tiêu đề công khai của bạn. Bạn có thể sử dụng công cụ này một cách an toàn trong
mã ngoại tuyến, nhưng nếu bạn dựa vào __builtin_available
trong bất kỳ mã nào trong
tiêu đề, chẳng hạn như hàm cùng dòng hoặc định nghĩa mẫu, bạn buộc tất cả
người tiêu dùng bật tính năng này. Cũng vì lý do đó mà chúng tôi không cho phép
theo mặc định trong NDK, bạn nên tránh đưa ra lựa chọn đó thay mặt
của người tiêu dùng.
Nếu bạn cần thực hiện hành vi này trong tiêu đề công khai, hãy nhớ ghi lại để người dùng của bạn đều biết rằng họ cần bật tính năng này và nhận thức được rủi ro khi làm như vậy.