Управление видимостью символов может уменьшить размер APK, сократить время загрузки и помочь другим разработчикам избежать случайных зависимостей от деталей реализации. Самый надежный способ сделать это — использовать сценарии версий.
Скрипты версий — это функция компоновщиков ELF, которую можно использовать как более надежную форму -fvisibility=hidden
. Более подробное объяснение см. в разделе «Преимущества» ниже или читайте дальше, чтобы узнать, как использовать сценарии версий в своем проекте.
В документации GNU, указанной выше, и в нескольких других местах на этой странице вы увидите ссылки на «версии символов». Это потому, что первоначальная цель этих файлов заключалась в том, чтобы позволить нескольким версиям символа (обычно функции) существовать в одной библиотеке для сохранения совместимости с ошибками в библиотеках. Android также поддерживает такое использование, но обычно оно полезно только поставщикам библиотек ОС, и даже мы не используем их в Android, поскольку targetSdkVersion
предлагает те же преимущества с более продуманным процессом согласия. Что касается темы этого документа, не беспокойтесь о таких терминах, как «версия символа». Если вы не определяете несколько версий одного и того же символа, «версия символа» — это просто группа символов с произвольным именем в файле.
Если вы являетесь автором библиотеки (независимо от того, ваш интерфейс — C/C++ или Java/Kotlin, а ваш собственный код — это всего лишь деталь реализации), а не разработчиком приложений, обязательно прочитайте также «Советы для поставщиков промежуточного программного обеспечения» .
Напишите сценарий версии
В идеальном случае приложение (или AAR), включающее собственный код, будет содержать ровно одну общую библиотеку со всеми ее зависимостями, статически связанными с этой одной библиотекой, а полный общедоступный интерфейс этой библиотеки — JNI_OnLoad
. Это позволяет применять преимущества, описанные на этой странице, максимально широко. В этом случае, предполагая, что библиотека называется libapp.so
, создайте файл libapp.map.txt
(имя не обязательно должно совпадать, а суффикс .map.txt
является всего лишь соглашением) со следующим содержимым (вы можете опусти комментарии):
# 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.
*;
};
Если в вашем приложении имеется несколько общих библиотек, необходимо добавить по одному сценарию версии для каждой библиотеки.
Для библиотек JNI, которые не используют JNI_OnLoad
и RegisterNatives()
, вы можете вместо этого перечислить каждый из методов JNI с их искаженными именами JNI.
Для библиотек, отличных от JNI (обычно зависимостей библиотек JNI), вам необходимо перечислить всю поверхность API. Если ваш интерфейс C++, а не C, вы можете использовать extern "C++" { ... }
в сценарии версии так же, как и в файле заголовка. Например:
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:
*;
};
Используйте сценарий версии при сборке
Сценарий версии должен быть передан компоновщику при сборке. Следуйте инструкциям, соответствующим вашей системе сборки, приведенным ниже.
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-сборка
# 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.
Другой
Если используемая вами система сборки имеет явную поддержку сценариев версий, используйте ее.
В противном случае используйте следующие флаги компоновщика:
-Wl,--version-script,path/to/libapp.map.txt -Wl,--no-version-undefined
То, как они указаны, будет зависеть от вашей системы сборки, но обычно есть опция с именем LDFLAGS
или что-то подобное. path/to/libapp.map.txt
должен быть разрешимым из текущего рабочего каталога компоновщика. Зачастую проще использовать абсолютный путь.
Если вы не используете систему сборки или являетесь специалистом по сопровождению системы сборки, желающим добавить поддержку сценариев версий, эти флаги следует передавать в clang
(или clang++
) при компоновке, но не при компиляции.
Преимущества
Размер APK можно увеличить при использовании сценария версии, поскольку он минимизирует видимый набор символов в библиотеке. Сообщая компоновщику, какие именно функции доступны вызывающим объектам, компоновщик может удалить весь недоступный код из библиотеки. Этот процесс представляет собой разновидность устранения мертвого кода . Компоновщик не может удалить определение функции (или другого символа), которое не скрыто, даже если функция никогда не вызывается, поскольку компоновщик должен предполагать, что видимый символ является частью общедоступного интерфейса библиотеки. Скрытие символов позволяет компоновщику удалять функции, которые не вызываются, уменьшая размер библиотеки.
Производительность загрузки библиотеки повышается по тем же причинам: для видимых символов требуются перемещения , поскольку эти символы являются взаимозаменяемыми . Это почти никогда не является желаемым поведением, но оно требуется спецификацией ELF, поэтому оно используется по умолчанию. но поскольку компоновщик не может знать, какие символы (если таковые имеются) вы собираетесь вставлять, он должен создавать перемещения для каждого видимого символа. Скрытие этих символов позволяет компоновщику пропустить эти перемещения в пользу прямых переходов, что уменьшает объем работы, которую динамический компоновщик должен выполнять при загрузке библиотек.
Явное перечисление вашей поверхности API также предотвращает ошибочную зависимость потребителей ваших библиотек от деталей реализации вашей библиотеки, поскольку эти детали не будут видны.
Сравнение с альтернативами
Скрипты версий дают аналогичные результаты в качестве альтернатив, таких как -fvisibility=hidden
или для каждой функции __attribute__((visibility("hidden")))
. Все три подхода контролируют, какие символы библиотеки видны другим библиотекам и dlsym
.
Самым большим недостатком двух других подходов является то, что они могут скрывать только символы, определенные в создаваемой библиотеке. Они не могут скрыть символы из статических зависимостей библиотеки. Очень распространенный случай, когда это имеет значение, — это использование libc++_static.a
. Даже если в вашей сборке используется -fvisibility=hidden
, а собственные символы библиотеки будут скрыты, все символы, включенные в libc++_static.a
станут общедоступными символами вашей библиотеки. Напротив, сценарии версий предлагают явный контроль над общедоступным интерфейсом библиотеки; если символ явно не указан как видимый в сценарии версии, он будет скрыт.
Другое отличие может быть как плюсом, так и минусом: общедоступный интерфейс библиотеки должен быть явно определен в скрипте версии. Для библиотек JNI это на самом деле тривиально, поскольку единственным необходимым интерфейсом для библиотеки JNI является JNI_OnLoad
(поскольку методы JNI, зарегистрированные с помощью RegisterNatives()
не обязательно должны быть общедоступными). Для библиотек с большим общедоступным интерфейсом это может стать дополнительным бременем по обслуживанию, но обычно оно того стоит.