Sử dụng API mới

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()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()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()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 vkBindBufferMemory2libvulkan.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 dlopendlsym 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.