Kiểm soát chế độ hiển thị biểu tượng

Việc kiểm soát chế độ hiển thị biểu tượng có thể làm giảm kích thước APK, cải thiện thời gian tải và giúp các nhà phát triển khác tránh các phần phụ thuộc ngoài ý muốn về thông tin triển khai. Cách hiệu quả nhất để làm việc này là sử dụng tập lệnh phiên bản.

Tập lệnh phiên bản là một tính năng của trình liên kết ELF có thể được dùng dưới dạng một hình thức mạnh mẽ hơn của -fvisibility=hidden. Hãy xem phần Lợi ích bên dưới để biết thêm thông tin giải thích chi tiết hoặc đọc tiếp để tìm hiểu cách sử dụng tập lệnh phiên bản trong dự án.

Trong tài liệu GNU được liên kết ở trên và ở một vài vị trí khác trên trang này, bạn sẽ thấy các thông tin tham chiếu đến "phiên bản biểu tượng". Đó là do ý định ban đầu của các tệp này là cho phép nhiều phiên bản của một ký hiệu (thường là một hàm) tồn tại trong một thư viện duy nhất để bảo tồn khả năng tương thích với lỗi trong thư viện. Android cũng hỗ trợ cách sử dụng đó, nhưng thường chỉ được sử dụng cho các nhà cung cấp thư viện hệ điều hành và thậm chí chúng tôi không sử dụng chúng trong Android vì targetSdkVersion mang đến các lợi ích tương tự với quy trình chọn tham gia có tính toán thận trọng hơn. Đối với chủ đề của tài liệu này, bạn đừng lo lắng về những từ như "phiên bản biểu tượng". Nếu bạn không xác định nhiều phiên bản của cùng một ký hiệu, thì "phiên bản ký hiệu" chỉ là một nhóm ký hiệu được đặt tên tuỳ ý trong tệp.

Nếu bạn là tác giả thư viện (cho dù giao diện của bạn là C/C++ hay Java/Kotlin và mã gốc chỉ là chi tiết triển khai) thay vì nhà phát triển ứng dụng, hãy nhớ đọc bài viết Lời khuyên cho nhà cung cấp phần mềm trung gian.

Viết tập lệnh phiên bản

Trong trường hợp lý tưởng, một ứng dụng (hoặc AAR) có chứa mã gốc sẽ chứa đúng một thư viện dùng chung, với tất cả các phần phụ thuộc được liên kết tĩnh vào thư viện đó và giao diện công khai đầy đủ của thư viện đó là JNI_OnLoad. Điều này cho phép áp dụng các lợi ích được mô tả trên trang này một cách rộng rãi nhất có thể. Trong trường hợp đó, giả sử thư viện có tên là libapp.so, hãy tạo một tệp libapp.map.txt (tên không cần khớp và hậu tố .map.txt chỉ là quy ước) với nội dung sau (bạn có thể bỏ qua các nhận xét):

# The name used here also doesn't matter. This is the name of the "version"
# which matters when the version script is actually used to create multiple
# versions of the same symbol, but that's not what we're doing.
LIBAPP {
  global:
    # Every symbol named in this section will have "default" (that is, public)
    # visibility. See below for how to refer to C++ symbols without mangling.
    JNI_OnLoad;
  local:
    # Every symbol in this section will have "local" (that is, hidden)
    # visibility. The wildcard * is used to indicate that all symbols not listed
    # in the global section should be hidden.
    *;
};

Nếu ứng dụng của bạn có nhiều thư viện dùng chung, bạn phải thêm một tập lệnh phiên bản cho mỗi thư viện.

Đối với các thư viện JNI không sử dụng JNI_OnLoadRegisterNatives(), bạn có thể liệt kê từng phương thức JNI với tên được cắt bớt JNI của các phương thức đó.

Đối với các thư viện không phải JNI (thường là các phần phụ thuộc của thư viện JNI), bạn cần liệt kê toàn bộ giao diện API. Nếu giao diện của bạn là C++ thay vì C, bạn có thể sử dụng extern "C++" { ... } trong tập lệnh phiên bản giống như trong tệp tiêu đề. Ví dụ:

LIBAPP {
  global:
    extern "C++" {
      # A class that exposes only some methods. Note that any methods that are
      # `private` in the class will still need to be visible in the library if
      # they are called by `inline` or `template` functions.
      #
      # Non-static members do not need to be enumerated as they do not have
      # symbols associated with them, but static members must be included.
      #
      # The * exposes all overloads of the MyClass constructor, but note that it
      # will also expose methods like MyClass::MyClassNonConstructor.
      MyClass::MyClass*;
      MyClass::DoSomething;
      MyClass::static_member;

      # All members/methods of a class, including those that are `private` in
      # the class.
      MyOtherClass::*;
      #

      # If you wish to only expose some overloads, name the full signature.
      # You'll need to wrap the name in quotes, otherwise you'll get a warning
      # like like "ignoring invalid character '(' in script" and the symbol will
      # remain hidden (pass -Wl,--no-undefined-version to convert that warning
      # to an error as described below).
      "MyClass::MyClass()";
      "MyClass::MyClass(const MyClass&)";
      "MyClass::~MyClass()";
    };
  local:
    *;
};

Sử dụng tập lệnh phiên bản khi tạo bản dựng

Bạn phải truyền tập lệnh phiên bản đến trình liên kết khi tạo bản dựng. Hãy làm theo các bước phù hợp với hệ thống xây dựng của bạn ở bên dưới.

CMake

# Assuming that your app library's target is named "app":
target_link_options(app
    PRIVATE
    -Wl,--version-script,${CMAKE_SOURCE_DIR}/libapp.map.txt
    # This causes the linker to emit an error when a version script names a
    # symbol that is not found, rather than silently ignoring that line.
    -Wl,--no-undefined-version
)

# Without this, changes to the version script will not cause the library to
# relink.
set_target_properties(app
    PROPERTIES
    LINK_DEPENDS ${CMAKE_SOURCE_DIR}/libapp.map.txt
)

ndk-build

# Add to an existing `BUILD_SHARED_LIBRARY` stanza (use `+=` instead of `:=` if
# the module already sets `LOCAL_LDFLAGS`):
LOCAL_LDFLAGS := -Wl,--version-script,$(LOCAL_PATH)/libapp.map.txt

# This causes the linker to emit an error when a version script names a symbol
# that is not found, rather than silently ignoring that line.
LOCAL_ALLOW_UNDEFINED_VERSION_SCRIPT_SYMBOLS := false

# ndk-build doesn't have a mechanism for specifying that libapp.map.txt is a
# dependency of the module. You may need to do a clean build or otherwise force
# the library to rebuild (such as by changing a source file) when altering the
# version script.

Khác

Nếu hệ thống xây dựng bạn đang sử dụng có hỗ trợ rõ ràng cho tập lệnh phiên bản, hãy sử dụng hệ thống đó.

Nếu không, hãy sử dụng các cờ trình liên kết sau:

-Wl,--version-script,path/to/libapp.map.txt -Wl,--no-version-undefined

Cách chỉ định các tuỳ chọn đó sẽ phụ thuộc vào hệ thống xây dựng của bạn, nhưng thường sẽ có một tuỳ chọn có tên là LDFLAGS hoặc một tuỳ chọn tương tự. path/to/libapp.map.txt cần được phân giải từ thư mục đang hoạt động hiện tại của trình liên kết. Sử dụng đường dẫn tuyệt đối thường sẽ dễ dàng hơn.

Nếu bạn không sử dụng hệ thống xây dựng hoặc là người duy trì hệ thống xây dựng muốn thêm tính năng hỗ trợ tập lệnh phiên bản, thì các cờ đó phải được truyền đến clang (hoặc clang++) khi liên kết nhưng không phải khi biên dịch.

Lợi ích

Bạn có thể cải thiện kích thước APK khi sử dụng tập lệnh phiên bản vì tập lệnh này giúp giảm thiểu tập hợp các ký hiệu hiển thị trong thư viện. Bằng cách cho trình liên kết biết chính xác những hàm nào mà phương thức gọi có thể truy cập, trình liên kết có thể xoá tất cả mã không truy cập được khỏi thư viện. Quá trình này là một loại loại bỏ mã chết. Trình liên kết không thể xoá định nghĩa cho hàm (hoặc biểu tượng khác) không bị ẩn, ngay cả khi hàm không bao giờ được gọi, vì trình liên kết phải giả định rằng biểu tượng hiển thị là một phần của giao diện công khai của thư viện. Việc ẩn các ký hiệu cho phép trình liên kết xoá các hàm không được gọi, giảm kích thước của thư viện.

Hiệu suất tải thư viện được cải thiện vì những lý do tương tự: di chuyển là bắt buộc đối với các ký hiệu hiển thị vì các ký hiệu đó là có thể kết hợp. Đó gần như không phải là hành vi mong muốn, nhưng đó là hành vi bắt buộc theo quy cách ELF, vì vậy, đó là hành vi mặc định. Tuy nhiên, vì trình liên kết không thể biết bạn định chèn biểu tượng nào (nếu có), nên trình liên kết phải tạo các lượt di chuyển cho mọi biểu tượng hiển thị. Việc ẩn các ký hiệu đó cho phép trình liên kết bỏ qua các lượt chuyển vị trí đó để thay thế các lệnh chuyển trực tiếp, giúp giảm lượng công việc mà trình liên kết động phải thực hiện khi tải thư viện.

Việc liệt kê rõ ràng giao diện API cũng ngăn người dùng thư viện vô tình phụ thuộc vào thông tin triển khai của thư viện, vì những thông tin đó sẽ không hiển thị.

So sánh với các giải pháp thay thế

Tập lệnh phiên bản cung cấp kết quả tương tự như các phương án thay thế như -fvisibility=hidden hoặc __attribute__((visibility("hidden"))) cho mỗi hàm. Cả ba phương pháp đều kiểm soát những biểu tượng của thư viện nào hiển thị với các thư viện khác và với dlsym.

Nhược điểm lớn nhất của hai phương pháp còn lại là chúng chỉ có thể ẩn các ký hiệu được xác định trong thư viện đang được tạo. Các lớp này không thể ẩn các biểu tượng khỏi các phần phụ thuộc thư viện tĩnh của thư viện. Một trường hợp rất phổ biến mà điều này tạo ra sự khác biệt là khi sử dụng libc++_static.a. Ngay cả khi bản dựng của bạn sử dụng -fvisibility=hidden, trong khi các ký hiệu riêng của thư viện sẽ bị ẩn, tất cả các ký hiệu có trong libc++_static.a sẽ trở thành ký hiệu công khai của thư viện. Ngược lại, tập lệnh phiên bản cung cấp quyền kiểm soát rõ ràng đối với giao diện công khai của thư viện; nếu không được liệt kê rõ ràng là hiển thị trong tập lệnh phiên bản, thì ký hiệu sẽ bị ẩn.

Sự khác biệt khác có thể là cả ưu và khuyết điểm: giao diện công khai của thư viện phải được xác định rõ ràng trong tập lệnh phiên bản. Đối với các thư viện JNI, việc này thực sự không quan trọng vì giao diện cần thiết duy nhất cho thư viện JNI là JNI_OnLoad (vì các phương thức JNI đã đăng ký với RegisterNatives() không cần công khai). Đối với các thư viện có giao diện công khai lớn, đây có thể là gánh nặng bảo trì bổ sung, nhưng thường đáng để thực hiện.