Mengontrol visibilitas simbol dapat mengurangi ukuran APK, meningkatkan waktu pemuatan, dan membantu developer lain menghindari dependensi yang tidak disengaja pada detail implementasi. Cara yang paling andal untuk melakukannya adalah dengan skrip versi.
Skrip versi adalah fitur penaut ELF yang dapat digunakan sebagai bentuk
-fvisibility=hidden
yang lebih andal. Lihat Manfaat di bawah untuk penjelasan yang lebih mendetail, atau baca terus untuk mempelajari cara menggunakan skrip versi dalam project Anda.
Dalam dokumentasi GNU yang ditautkan di atas dan di beberapa tempat
lain di halaman ini, Anda akan melihat referensi ke "versi simbol". Hal ini karena
tujuan awal file ini adalah untuk memungkinkan beberapa versi simbol
(biasanya fungsi) ada dalam satu library untuk mempertahankan kompatibilitas
bug di library. Android juga mendukung penggunaan tersebut, tetapi umumnya
hanya berguna bagi vendor library OS, dan bahkan kami tidak menggunakannya di Android karena
targetSdkVersion
menawarkan manfaat yang sama dengan proses keikutsertaan
yang lebih disengaja. Untuk topik dokumen ini, jangan khawatir dengan istilah seperti "versi
simbol". Jika Anda tidak menentukan beberapa versi simbol yang sama, "versi
simbol" hanyalah pengelompokan simbol yang dinamai arbitrer dalam file.
Jika Anda adalah penulis library (baik antarmuka Anda adalah C/C++, atau Java/Kotlin dan kode native Anda hanyalah detail implementasi), bukan developer aplikasi, pastikan untuk juga membaca Saran untuk vendor middleware.
Menulis skrip versi
Dalam kasus ideal, aplikasi (atau AAR) yang menyertakan kode native akan berisi
tepat satu library bersama, dengan semua dependensinya ditautkan secara statis ke
satu library tersebut, dan antarmuka publik lengkap library tersebut adalah
JNI_OnLoad
. Hal ini memungkinkan manfaat yang dijelaskan di halaman ini diterapkan
seseluas mungkin. Dalam hal ini, dengan asumsi bahwa library tersebut bernama
libapp.so
, buat file libapp.map.txt
(namanya tidak perlu cocok, dan
akhiran .map.txt
hanyalah konvensi) dengan konten berikut (Anda dapat
menghapus komentar):
# 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.
*;
};
Jika aplikasi Anda memiliki lebih dari satu library bersama, Anda harus menambahkan satu skrip versi per library.
Untuk library JNI yang tidak menggunakan JNI_OnLoad
dan RegisterNatives()
, Anda
dapat mencantumkan setiap metode JNI dengan nama JNI yang di-mangle.
Untuk library non-JNI (biasanya dependensi library JNI), Anda harus
mengumpukan platform API lengkap. Jika antarmuka Anda menggunakan C++, bukan C, Anda dapat
menggunakan extern "C++" { ... }
dalam skrip versi dengan cara yang sama seperti pada
file header. Contoh:
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:
*;
};
Menggunakan skrip versi saat mem-build
Skrip versi harus diteruskan ke penaut saat mem-build. Ikuti langkah-langkah yang sesuai dengan sistem build Anda di bawah.
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.
Lainnya
Jika sistem build yang Anda gunakan memiliki dukungan eksplisit untuk skrip versi, gunakan sistem tersebut.
Jika tidak, gunakan flag penaut berikut:
-Wl,--version-script,path/to/libapp.map.txt -Wl,--no-version-undefined
Cara menentukannya akan bergantung pada sistem build Anda, tetapi biasanya
ada opsi bernama LDFLAGS
atau yang serupa. path/to/libapp.map.txt
harus
dapat di-resolve dari direktori kerja penaut saat ini. Sering kali
lebih mudah menggunakan jalur absolut.
Jika Anda tidak menggunakan sistem build, atau merupakan pengelola sistem build yang ingin
menambahkan dukungan skrip versi, flag tersebut harus diteruskan ke clang
(atau
clang++
) saat melakukan penautan, tetapi tidak saat mengompilasi.
Manfaat
Ukuran APK dapat ditingkatkan saat menggunakan skrip versi karena meminimalkan kumpulan simbol yang terlihat dalam library. Dengan memberi tahu linker secara tepat fungsi mana yang dapat diakses oleh pemanggil, penaut dapat menghapus semua kode yang tidak dapat dijangkau dari library. Proses ini adalah jenis penghapusan kode mati. Linker tidak dapat menghapus definisi untuk fungsi (atau simbol lain) yang tidak disembunyikan, meskipun fungsi tidak pernah dipanggil, karena penaut harus mengasumsikan bahwa simbol yang terlihat adalah bagian dari antarmuka publik library. Menyembunyikan simbol memungkinkan linker menghapus fungsi yang tidak dipanggil, sehingga mengurangi ukuran library.
Performa pemuatan library ditingkatkan karena alasan serupa: relokasi diperlukan untuk simbol yang terlihat karena simbol tersebut dapat di-interpolasikan. Hal ini hampir tidak pernah menjadi perilaku yang diinginkan, tetapi merupakan hal yang diperlukan oleh spesifikasi ELF, sehingga merupakan default. Namun, karena penaut tidak dapat mengetahui simbol mana (jika ada) yang ingin Anda masukkan, penaut harus membuat relokasi untuk setiap simbol yang terlihat. Menyembunyikan simbol tersebut memungkinkan penaut menghapus relokasi tersebut untuk mendukung lompatan langsung, yang mengurangi jumlah pekerjaan yang harus dilakukan penaut dinamis saat memuat library.
Mengenumerasikan platform API secara eksplisit juga mencegah konsumen library Anda tergantung secara keliru pada detail implementasi library Anda, karena detail tersebut tidak akan terlihat.
Perbandingan dengan alternatif
Skrip versi menawarkan hasil yang serupa dengan alternatif seperti
-fvisibility=hidden
atau __attribute__((visibility("hidden")))
per fungsi.
Ketiga pendekatan tersebut mengontrol simbol library mana yang terlihat oleh library
lain dan dlsym
.
Kelemahan terbesar dari dua pendekatan lainnya adalah keduanya hanya dapat
menyembunyikan simbol yang ditentukan dalam library yang sedang dibuat. Library ini tidak dapat menyembunyikan simbol dari
dependensi library statis library. Kasus yang sangat umum yang membuat
perbedaan adalah saat menggunakan libc++_static.a
. Meskipun build Anda menggunakan
-fvisibility=hidden
, sementara simbol library itu sendiri akan disembunyikan, semua
simbol yang disertakan dari libc++_static.a
akan menjadi simbol publik library
Anda. Sebaliknya, skrip versi menawarkan kontrol eksplisit atas antarmuka publik
library; jika simbol tidak tercantum secara eksplisit sebagai terlihat dalam
skrip versi, simbol tersebut akan disembunyikan.
Perbedaan lainnya dapat berupa pro dan kontra: antarmuka publik library
harus ditentukan secara eksplisit dalam skrip versi. Untuk library JNI, hal ini
sebenarnya tidak penting, karena satu-satunya antarmuka yang diperlukan untuk library JNI adalah
JNI_OnLoad
(karena metode JNI yang terdaftar dengan RegisterNatives()
tidak perlu
publik). Untuk library dengan antarmuka publik yang besar, hal ini dapat menjadi
beban pemeliharaan tambahan, tetapi biasanya hal ini bermanfaat.