Hướng dẫn về trình bao bọc thư viện

Hướng dẫn này mô tả cách sử dụng trình bao bọc thư viện API Android. Công cụ dòng lệnh của trình bao bọc thư viện sẽ tạo mã bao bọc ở ngôn ngữ C cho API Android Java, cho phép bạn tích hợp thư viện Java vào các ứng dụng Android C/C++ gốc. Để biết thêm thông tin chi tiết về trình bao bọc thư viện, hãy xem bài viết Trình bao bọc thư viện dành cho API Android.

Hướng dẫn từng bước này minh hoạ cách sử dụng công cụ bao bọc để tích hợp thư viện Java vào một ứng dụng Android gốc. Với mục đích lấy ví dụ, hướng dẫn này đề cập đến việc tích hợp thư viện thông báo của gói androidx.core.app. Hãy xem bài viết Tạo thông báo để tìm hiểu thêm về thư viện này.

Điều kiện tiên quyết

Hướng dẫn này giả định rằng bạn đã có một dự án Android gốc. Dự án này cũng dùng hệ thống xây dựng Gradle. Nếu bạn chưa có dự án nào, hãy tạo một dự án mới trong Android Studio bằng mẫu C++ gốc.

Mã ví dụ trong hướng dẫn này sử dụng gốc thư mục my_project/. Mã gốc nằm trong my_project/app/src/main/cpp/, là thư mục mặc định của các dự án Android Studio.

Nếu bạn chưa có công cụ bao bọc thư viện, hãy tải xuống và giải nén gói vào thư mục tuỳ ý. Công cụ CLI này yêu cầu Môi trường chạy Java (JRE).

Tạo mã gốc

Khi tích hợp thư viện Java, hãy dùng công cụ bao bọc để tạo một trình bao bọc mã gốc. Bước đầu tiên cần làm là định cấu hình trình bao bọc.

Tạo cấu hình trình bao bọc

Bạn tạo tệp cấu hình của trình bao bọc thư viện để kiểm soát đầu ra của trình tạo mã gốc. Tệp này có một tính năng cho phép bạn chỉ định các lớp và phương thức để tạo mã bao bọc.

Vì không có nhiều phương thức để bao bọc thư viện thông báo, nên bạn có thể xác định các phương thức đó ngay trong phần custom_classes. Tạo một tài nguyên config.json mới ở bất kỳ đâu trong dự án để xác định các phương thức. Ví dụ: bạn có thể tạo my_project/library_wrapper/config.json và dán cấu hình mẫu sau đây:

{
  "custom_classes": [
    {
      "class_name": "class java.lang.CharSequence"
    },
    {
      "class_name": "class java.lang.Object",
      "methods": [
        "java.lang.String toString()"
      ]
    },
    {
      "class_name": "class java.lang.String"
    },
    {
      "class_name": "class android.content.Context",
      "methods": [
        "java.lang.Object getSystemService(java.lang.String name)"
      ]
    },
    {
      "class_name": "class android.app.Notification"
    },
    {
      "class_name": "class android.app.NotificationManager",
      "methods": [
        "void createNotificationChannel(android.app.NotificationChannel channel)"
      ]
    },
    {
      "class_name": "class android.app.NotificationChannel",
      "methods": [
        "NotificationChannel(java.lang.String id, java.lang.CharSequence name, int importance)",
        "void setDescription(java.lang.String description)"
      ]
    },
    {
      "class_name": "class androidx.core.app.NotificationCompat"
    },
    {
      "class_name": "class androidx.core.app.NotificationCompat$Builder",
      "methods": [
        "Builder(android.content.Context context, java.lang.String channelId)",
        "androidx.core.app.NotificationCompat$Builder setContentText(java.lang.CharSequence text)",
        "androidx.core.app.NotificationCompat$Builder setContentTitle(java.lang.CharSequence title)",
        "androidx.core.app.NotificationCompat$Builder setSmallIcon(int icon)",
        "androidx.core.app.NotificationCompat$Builder setPriority(int pri)",
        "android.app.Notification build()"
      ]
    },
    {
      "class_name": "class androidx.core.app.NotificationManagerCompat",
      "methods": [
        "static androidx.core.app.NotificationManagerCompat from(android.content.Context context)",
        "void notify(int id, android.app.Notification notification)"
      ]
    }
  ]
}

Trong mẫu trước đó, bạn trực tiếp khai báo các lớp và phương thức Java cần có mã bao bọc gốc.

Chạy trình bao bọc thư viện

Khi xác định xong tệp cấu hình trình bao bọc, bạn đã sẵn sàng sử dụng công cụ để tạo mã bao bọc gốc. Mở thiết bị đầu cuối mà bạn đã trích xuất trình bao bọc thư viện rồi chạy lệnh sau:

java -jar lw.jar \
  -o "my_project/app/src/main/cpp/native_wrappers" \
  -c "my_project/library_wrapper/config.json"

Trong mẫu ở trên, bạn dùng tham số -c để chỉ định vị trí của cấu hình trình bao bọc và tham số -o để xác định thư mục mã được tạo. Sau khi chạy công cụ, bạn sẽ có mã được tạo cần thiết để gọi API thông báo dựa trên Java từ ứng dụng gốc.

Triển khai thông báo gốc

Trong phần này, bạn sẽ tích hợp thư viện thông báo của Android vào ứng dụng gốc bằng mã bao bọc được tạo. Bước đầu tiên là cập nhật tài nguyên gradle.build ở cấp ứng dụng của dự án (my_project/app/gradle.build).

Cập nhật gradle.build

  1. GNI là thư viện hỗ trợ cần thiết cho mã bao bọc được tạo. Tất cả dự án dùng mã được tạo đều phải tham chiếu thư viện này. Để tham chiếu thư viện này, hãy thêm dòng sau vào phần dependencies của build.gradle:

    implementation 'com.google.android.gms:play-services-gni-native-c:1.0.0-beta2'
    
  2. Để bật tính năng hỗ trợ prefab, hãy thêm mã sau vào phần android:

    buildFeatures {
      prefab true
    }
    
  3. Để định cấu hình cmake, hãy dùng cấu hình cmake sau trong phần android/defaultConfig:

    externalNativeBuild {
      cmake {
          arguments '-DANDROID_STL=c++_shared'
      }
    }
    

Cấu hình build.gradle hoàn chỉnh của bạn sẽ có dạng như sau:

android {
    ...

    buildFeatures {
        prefab true
    }

    defaultConfig {
        ...

        externalNativeBuild {
            cmake {
                arguments '-DANDROID_STL=c++_shared'
            }
        }
    }
}

dependencies {
    ...
    implementation 'com.google.android.gms:play-services-gni-native-c:1.0.0-beta2'
    ...
}

Chỉnh sửa CMakeLists

  1. Thêm thư viện GNI vào CMakeLists.txt (my_project/app/src/main/cpp/CMakeLists.txt) của dự án bằng cách thêm dòng sau ở cấp cao nhất của tệp:

    find_package(com.google.android.gms.gni.c REQUIRED CONFIG)
    
  2. Thêm dòng sau vào phần target_link_libraries:

    PUBLIC com.google.android.gms.gni.c::gni_shared
    
  3. Thêm nội dung tham chiếu đến mã được tạo bằng cách thêm dòng sau ở cấp cao nhất của tệp:

    file(GLOB_RECURSE native_wrappers CONFIGURE_DEPENDS "native_wrappers/*.cpp" "native_wrappers/*.cc")
    
  4. Thêm các dòng sau vào gần cuối tệp:

    include_directories(./native_wrappers/c)
    include_directories(./native_wrappers/cpp)
    

Tài nguyên CMakeLists.txt đã cập nhật phải tương tự với mẫu sau:

cmake_minimum_required(VERSION 3.18.1)

project("my_project")

file(GLOB_RECURSE native_wrappers CONFIGURE_DEPENDS "native_wrappers/*.cpp" "native_wrappers/*.cc")

add_library(
        my_project
        SHARED
        native-lib.cpp
        ${native_wrappers}
        )

find_library(
        log-lib
        log)

find_package(com.google.android.gms.gni.c REQUIRED CONFIG)

target_link_libraries(
        my_project
        PUBLIC com.google.android.gms.gni.c::gni_shared
        ${log-lib})

include_directories(./native_wrappers/c)
include_directories(./native_wrappers/cpp)

Triển khai logic thông báo

  1. Mở hoặc tạo tệp nguồn mà bạn muốn triển khai chức năng thông báo. Trong tệp này, hãy thêm tệp tiêu đề gni.h và xác định một hàm ShowNativeNotification() mới:

    #include "gni/gni.h"
    
    void ShowNativeNotification(JNIEnv *env, jobject main_activity, int icon_id) {
      // Get the JavaVM from the JNIEnv.
      JavaVM *java_vm;
      env->GetJavaVM(&java_vm);
    
      // Initialize the GNI runtime. This function needs to be called before any
      // call to the generated code.
      GniCore_init(java_vm, main_activity);
    }
    
  2. Tiếp theo, hãy xác định các giá trị hằng số dành riêng cho thông báo cũng như hàm xử lý thông báo CharSequenceFromCString()CreateNotification():

    C

    const int32_t IMPORTANCE_HIGH = 4;  // NotificationManager.IMPORTANCE_HIGH
    const int32_t PRIORITY_MAX = 2;  // NotificationCompat.PRIORITY_MAX
    const int32_t NOTIFICATION_ID = 123;  // User defined notification id.
    
    // Convert a C string into CharSequence.
    CharSequence *CharSequenceFromCString(const char *text) {
       String *string = String_fromCString(text);
       // Cast String to CharSequence. In Java, a String implements CharSequence.
       CharSequence *result = GNI_CAST(CharSequence, String, string);
       // Casting creates a new object, so it needs to be destroyed as normal.
       String_destroy(string);
       return result;
    }
    
    // Create a notification.
    Notification *
    CreateNotification(Context *context, String *channel_id,
                       const char *title, const char *content,
                       int32_t icon_id) {
       // Convert C strings to CharSequence.
       CharSequence *title_chars = CharSequenceFromCString(title);
       CharSequence *content_chars = CharSequenceFromCString(content);
    
       // Create a NotificationCompat.Builder and set all required properties.
       NotificationCompat_Builder *notification_builder =
           NotificationCompat_Builder_construct(context, channel_id);
       NotificationCompat_Builder_setContentTitle(notification_builder,
                                                  title_chars);
       NotificationCompat_Builder_setContentText(notification_builder,
                                                 content_chars);
       NotificationCompat_Builder_setSmallIcon(notification_builder, icon_id);
       NotificationCompat_Builder_setPriority(notification_builder,
                                              PRIORITY_MAX);
    
       // Build a notification.
       Notification *notification =
           NotificationCompat_Builder_build(notification_builder);
    
       // Clean up allocated objects.
       NotificationCompat_Builder_destroy(notification_builder);
       CharSequence_destroy(title_chars);
       CharSequence_destroy(content_chars);
    
       return notification;
    }
    

    C++

    const int32_t IMPORTANCE_HIGH = 4;  // NotificationManager.IMPORTANCE_HIGH
    const int32_t PRIORITY_MAX = 2;  // NotificationCompat.PRIORITY_MAX
    const int32_t NOTIFICATION_ID = 123;  // User defined notification id.
    
    // Convert a C string into CharSequence.
    CharSequence *CharSequenceFromCString(const char *text) {
       String *string = String_fromCString(text);
       // Cast String to CharSequence. In Java, a String implements CharSequence.
       CharSequence *result = new CharSequence(string->GetImpl());
       // Casting creates a new object, so it needs to be destroyed as normal.
       String::destroy(string);
       return result;
    }
    
    // Create a notification.
    Notification&
    CreateNotification(Context *context, String *channel_id, const char *title,
                       const char *content, int32_t icon_id) {
       // Convert C strings to CharSequence.
       CharSequence *title_chars = CharSequenceFromCString(title);
       CharSequence *content_chars = CharSequenceFromCString(content);
    
       // Create a NotificationCompat.Builder and set all required properties.
    
       NotificationCompat::Builder *notification_builder = new NotificationCompat::Builder(*context, *channel_id);
       notification_builder->setContentTitle(*title_chars);
       notification_builder->setContentText(*content_chars);
       notification_builder->setSmallIcon(icon_id);
       notification_builder->setPriority(PRIORITY_MAX);
    
       // Build a notification.
       Notification& notification = notification_builder->build();
    
       // Clean up allocated objects.
       NotificationCompat::Builder::destroy(notification_builder);
       CharSequence::destroy(title_chars);
       CharSequence::destroy(content_chars);
    
       return notification;
    }
    

    Một số chức năng của thư viện thông báo sẽ lấy CharSequence thay vì String. Hàm CharSequenceFromCString() cho phép chuyển đổi giữa những đối tượng này. Hàm CreateNotification() sử dụng phiên bản đã bao bọc của NotificationCompat.Builder Java để tạo thông báo.

  3. Thêm logic để tạo kênh thông báo bằng cách dán vào hàm CreateNotificationChannel() như sau:

    C

    void CreateNotificationChannel(Context *context, String *channel_id) {
       CharSequence *channel_name = CharSequenceFromCString("channel name");
       String *channel_description = String_fromCString("channel description");
       String *system_service_name = String_fromCString("notification");
       NotificationChannel *channel =
           NotificationChannel_construct(channel_id, channel_name,
                                         IMPORTANCE_HIGH);
       NotificationChannel_setDescription(channel, channel_description);
    
       Object *notification_manager_as_object =
           Context_getSystemService(context, system_service_name);
       NotificationManager *notification_manager =
           GNI_CAST(NotificationManager, Object,
                    notification_manager_as_object);
    
       NotificationManager_createNotificationChannel(notification_manager,
                                                     channel);
    
       CharSequence_destroy(channel_name);
       String_destroy(channel_description);
       String_destroy(system_service_name);
       NotificationChannel_destroy(channel);
       Object_destroy(notification_manager_as_object);
       NotificationManager_destroy(notification_manager);
    }
    

    C++

    void CreateNotificationChannel(Context *context, String *channel_id) {
       CharSequence *channel_name = CharSequenceFromCString("channel name");
       String *channel_description = String_fromCString("channel description");
       String *system_service_name = String_fromCString("notification");
       NotificationChannel *channel =
           new NotificationChannel(*channel_id, *channel_name, IMPORTANCE_HIGH);
       channel->setDescription(*channel_description);
    
       Object& notification_manager_as_object =
           context->getSystemService(*system_service_name);
       NotificationManager *notification_manager =
           new NotificationManager(notification_manager_as_object.GetImpl());
    
       notification_manager->createNotificationChannel(*channel);
    
       CharSequence::destroy(channel_name);
       String::destroy(channel_description);
       String::destroy(system_service_name);
       NotificationChannel::destroy(channel);
       Object::destroy(&notification_manager_as_object);
       NotificationManager::destroy(notification_manager);
    }
    
  4. Cập nhật hàm ShowNativeNotification() mà bạn đã tạo trước đó để gọi CreateNotificationChannel(). Thêm mã sau vào cuối ShowNativeNotification():

    C

    void ShowNativeNotification(JNIEnv *env, jobject main_activity, int icon_id) {
     // ...
    
     // Create a Context object by wrapping an existing JNI reference.
     Context *context = Context_wrapJniReference(main_activity);
    
     // Create a String object.
     String *channel_id = String_fromCString("new_messages");
    
     // Create a notification channel.
     CreateNotificationChannel(context, channel_id);
    
     // Create a notification with a given title, content, and icon.
     Notification *notification =
         CreateNotification(context, channel_id, "My Native Notification",
                            "Hello!", icon_id);
    
     // Create a notification manager and use it to show the notification.
     NotificationManagerCompat *notification_manager =
         NotificationManagerCompat_from(context);
     NotificationManagerCompat_notify(notification_manager, NOTIFICATION_ID,
                                      notification);
    
     // Destroy all objects.
     Context_destroy(context);
     String_destroy(channel_id);
     Notification_destroy(notification);
     NotificationManagerCompat_destroy(notification_manager);
    }
    

    C++

    void ShowNativeNotification(JNIEnv *env, jobject main_activity, int icon_id) {
       // Get the JavaVM from the JNIEnv.
       JavaVM *java_vm;
       env->GetJavaVM(&java_vm);
    
       // Initialize the GNI runtime. This function needs to be called before any
       // call to the generated code.
       GniCore::Init(java_vm, main_activity);
    
       // Create a Context object by wrapping an existing JNI reference.
       Context *context = new Context(main_activity);
    
       // Create a String object.
       String *channel_id = String_fromCString("new_messages");
    
       // Create a notification channel.
       CreateNotificationChannel(context, channel_id);
    
       // Create a notification with a given title, content, and icon.
       Notification& notification =
           CreateNotification(context, channel_id, "My Native Notification",
                              "Hello!", icon_id);
    
       // Create a notification manager and use it to show the notification.
       NotificationManagerCompat& notification_manager =
           NotificationManagerCompat::from(*context);
       notification_manager.notify(NOTIFICATION_ID, notification);
    
       // Destroy all objects.
       Context::destroy(context);
       String::destroy(channel_id);
       Notification::destroy(&notification);
       NotificationManagerCompat::destroy(&notification_manager);
    }   
  5. Sau khi xác định logic, hãy kích hoạt thông báo bằng cách gọi ShowNativeNotification() tại một vị trí thích hợp trong dự án.

Chạy ứng dụng

Biên dịch và chạy mã gọi ShowNativeNotification(). Một thông báo đơn giản sẽ xuất hiện ở đầu màn hình của thiết bị kiểm thử.

Tạo trình bao bọc từ tệp JAR

Trong ví dụ trước, bạn đã dùng tệp cấu hình trình bao bọc để tự mình chỉ định các lớp và phương thức Java cần mã gốc. Đối với các trường hợp cần truy cập vào các phần lớn của API, bạn nên cung cấp một hoặc nhiều tệp JAR thư viện cho công cụ bao bọc vì như vậy sẽ hiệu quả hơn. Sau đó, trình bao bọc sẽ tạo ra các trình bao bọc cho mọi biểu tượng công khai tìm thấy trong tệp JAR.

Ví dụ sau đây sẽ bao bọc toàn bộ API Thông báo bằng cách cung cấp tệp JAR thư viện.

Lấy các tệp JAR bắt buộc

API thông báo là một phần của gói androidx.core có trong kho lưu trữ Google Maven. Tải tệp aar của thư viện xuống rồi giải nén tệp đó vào một thư mục tuỳ ý. Tìm tệp classes.jar.

Tệp classes.jar chứa nhiều lớp khác ngoài thư viện thông báo bắt buộc. Nếu bạn chỉ cung cấp trình bao bọc thư viện bằng classes.jar, công cụ sẽ tạo mã gốc cho mỗi lớp trong tệp JAR. Điều này không hiệu quả và không cần thiết cho dự án của chúng ta. Để giải quyết vấn đề này, hãy cung cấp một tệp bộ lọc cho cấu hình trình bao bọc để hạn chế việc tạo ra mã đối với các lớp thông báo của tệp JAR.

Xác định bộ lọc cho phép

Tệp bộ lọc là các tệp văn bản thuần tuý mà bạn cung cấp cho cấu hình trình bao bọc thư viện. Các loại tệp này cho phép bạn xác định những lớp cần đưa vào (hoặc loại trừ) khỏi các tệp JAR được cung cấp cho trình bao bọc thư viện.

Trong dự án của bạn, hãy tạo một tệp có tiêu đề allowed-symbols.txt rồi dán vào dòng sau:

androidx.core.app.NotificationCompat*

Khi được dùng làm bộ lọc cho phép, mã ở trên sẽ cho biết rằng chỉ những ký hiệu có tên bắt đầu bằng androidx.core.app.NotificationCompat mới được gói vào.

Chạy trình bao bọc thư viện

Mở một cửa sổ dòng lệnh đến thư mục JAR rồi chạy lệnh sau:

java -jar lw.jar \
 -i classes.jar \
 -o "./generated-jar" \
 -c "./config.json" \
 -fa allowed-symbols.txt \
 --skip_deprecated_symbols

Lệnh mẫu ở trên tạo mã bao bọc cho các lớp đã lọc vào thư mục generated-jar/.

Hỗ trợ

Nếu bạn gặp vấn đề với trình bao bọc thư viện, vui lòng cho chúng tôi biết.

Duyệt xem các lỗi Báo cáo lỗi
Kỹ thuật
Tài liệu