Sichtbarkeit von Symbolen steuern

Wenn Sie die Sichtbarkeit von Symbolen steuern, können Sie die APK-Größe reduzieren, die Ladezeiten verbessern und anderen Entwicklern helfen, versehentliche Abhängigkeiten von Implementierungsdetails zu vermeiden. Am besten eignen sich dafür Versionsscripts.

Versionsscripts sind eine Funktion von ELF-Linkern, die als robustere Form von -fvisibility=hidden verwendet werden können. Weitere Informationen finden Sie unten unter Vorteile. Unten erfahren Sie auch, wie Sie Versionsscripts in Ihrem Projekt verwenden.

In der oben verlinkten GNU-Dokumentation und an einigen anderen Stellen auf dieser Seite werden Verweise auf „Symbolversionen“ gemacht. Das liegt daran, dass diese Dateien ursprünglich dazu gedacht waren, mehrere Versionen eines Symbols (in der Regel einer Funktion) in einer einzigen Bibliothek zuzulassen, um die Fehlerkompatibilität in Bibliotheken zu erhalten. Android unterstützt diese Verwendung ebenfalls, sie ist jedoch in der Regel nur für Anbieter von Betriebssystembibliotheken von Nutzen. Wir verwenden sie in Android nicht, da targetSdkVersion dieselben Vorteile mit einem bewussteren Opt-in-Prozess bietet. Für das Thema dieses Dokuments müssen Sie sich keine Gedanken über Begriffe wie „Symbolversion“ machen. Wenn Sie nicht mehrere Versionen desselben Symbols definieren, ist „Symbolversion“ nur eine beliebig benannte Gruppierung von Symbolen in der Datei.

Wenn Sie Bibliotheksautor (unabhängig davon, ob Sie eine C/C++-Schnittstelle oder eine Java-/Kotlin-Version verwenden und Ihr nativer Code nur ein Implementierungsdetail ist) und kein App-Entwickler sind, sollten Sie auch die Tipps für Middleware-Anbieter lesen.

Versionsskript schreiben

Im Idealfall enthält eine App (oder AAR) mit nativem Code genau eine freigegebene Bibliothek, deren Abhängigkeiten alle statisch mit dieser Bibliothek verknüpft sind und deren vollständige öffentliche Schnittstelle JNI_OnLoad ist. So können die auf dieser Seite beschriebenen Vorteile möglichst breit angewendet werden. Angenommen, die Bibliothek heißt libapp.so, erstellen Sie in diesem Fall eine libapp.map.txt-Datei (der Name muss nicht übereinstimmen und das Suffix .map.txt ist nur eine Konvention) mit dem folgenden Inhalt (die Kommentare können Sie weglassen):

# 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.
    *;
};

Wenn Ihre App mehr als eine freigegebene Bibliothek hat, müssen Sie pro Bibliothek ein Versionsscript hinzufügen.

Bei JNI-Bibliotheken, für die JNI_OnLoad und RegisterNatives() nicht verwendet werden, können Sie stattdessen jede JNI-Methode mit ihren JNI-Mangled-Namen auflisten.

Bei nicht JNI-Bibliotheken (in der Regel Abhängigkeiten von JNI-Bibliotheken) müssen Sie die gesamte API-Oberfläche auflisten. Wenn Ihre Benutzeroberfläche C++ statt C ist, können Sie extern "C++" { ... } in einem Versionsscript genauso verwenden wie in einer Headerdatei. Beispiel:

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:
    *;
};

Versionsskript beim Erstellen verwenden

Das Versionsskript muss beim Erstellen an die Verknüpfung übergeben werden. Führen Sie die folgenden Schritte für Ihr Build-System aus.

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.

Sonstiges

Wenn das von Ihnen verwendete Build-System Versionsscripts explizit unterstützt, verwenden Sie diese.

Andernfalls verwenden Sie die folgenden Verknüpfungs-Flags:

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

Wie diese angegeben werden, hängt von Ihrem Build-System ab. Normalerweise gibt es jedoch eine Option namens LDFLAGS oder etwas Ähnliches. path/to/libapp.map.txt muss aus dem aktuellen Arbeitsverzeichnis des Linker aufgelöst werden können. Häufig ist es einfacher, einen absoluten Pfad zu verwenden.

Wenn Sie kein Build-System verwenden oder ein Administrator eines Build-Systems sind, der die Unterstützung von Versionsscripts hinzufügen möchte, sollten diese Flags beim Verknüpfen an clang (oder clang++) übergeben werden, aber nicht beim Kompilieren.

Vorteile

Die APK-Größe kann durch die Verwendung eines Versionsscripts verbessert werden, da dadurch die sichtbaren Symbole in einer Bibliothek minimiert werden. Wenn Sie dem Linker genau mitteilen, auf welche Funktionen die Aufrufer zugreifen können, kann er den gesamten nicht erreichbaren Code aus der Bibliothek entfernen. Dieser Prozess ist eine Art der Elimination von Totcode. Der Linker kann die Definition für eine Funktion (oder ein anderes Symbol) nicht entfernen, die nicht ausgeblendet ist, auch wenn die Funktion nie aufgerufen wird, da der Linker davon ausgehen muss, dass ein sichtbares Symbol Teil der öffentlichen Schnittstelle der Bibliothek ist. Durch das Ausblenden von Symbolen kann der Linker Funktionen entfernen, die nicht aufgerufen werden, wodurch die Größe der Bibliothek reduziert wird.

Die Ladeleistung der Bibliothek wird aus ähnlichen Gründen verbessert: Für sichtbare Symbole sind Umplatzierungen erforderlich, da diese Symbole austauschbar sind. Das ist fast nie das gewünschte Verhalten, aber von der ELF-Spezifikation gefordert. Es ist also die Standardeinstellung. Da die Verknüpfung aber nicht wissen kann, welche Symbole Sie ggf. vertauschen möchten, muss sie Verschiebungen für jedes sichtbare Symbol erstellen. Wenn diese Symbole ausgeblendet werden, kann die Verknüpfung diese Verschiebungen zugunsten von direkten Sprüngen weglassen. Dadurch verringert sich der Arbeitsaufwand, den die dynamische Verknüpfung beim Laden von Bibliotheken ausführen muss.

Die explizite Aufzählung Ihrer API-Oberfläche verhindert auch, dass Nutzer Ihrer Bibliotheken in Abhängigkeit von den Implementierungsdetails Ihrer Bibliothek versehentlich abgerufen werden, da diese Details nicht sichtbar sind.

Vergleich mit Alternativen

Versionsscripts bieten ähnliche Ergebnisse als Alternativen wie -fvisibility=hidden oder __attribute__((visibility("hidden"))) pro Funktion. Mit allen drei Ansätzen wird festgelegt, welche Symbole einer Bibliothek für andere Bibliotheken und für dlsym sichtbar sind.

Der größte Nachteil der beiden anderen Ansätze besteht darin, dass nur Symbole ausgeblendet werden können, die in der zu erstellenden Bibliothek definiert sind. Sie können keine Symbole aus den statischen Bibliotheksabhängigkeiten der Bibliothek ausblenden. Ein sehr häufiger Fall, in dem dies einen Unterschied macht, ist die Verwendung von libc++_static.a. Auch wenn in Ihrem Build -fvisibility=hidden verwendet wird, werden die eigenen Symbole der Bibliothek ausgeblendet. Alle Symbole aus libc++_static.a werden zu öffentlichen Symbolen Ihrer Bibliothek. Im Gegensatz dazu bieten Versionsscripts die explizite Kontrolle über die öffentliche Schnittstelle der Bibliothek. Wenn das Symbol im Versionsskript nicht explizit als sichtbar aufgeführt ist, wird es ausgeblendet.

Der andere Unterschied kann sowohl ein Vorteil als auch ein Nachteil sein: Die öffentliche Schnittstelle der Bibliothek muss in einem Versionsscript explizit definiert werden. Bei JNI-Bibliotheken ist das eigentlich trivial, da die einzige erforderliche Schnittstelle für eine JNI-Bibliothek JNI_OnLoad ist (da JNI-Methoden, die bei RegisterNatives() registriert sind, nicht öffentlich sein müssen). Für Bibliotheken mit einer großen öffentlichen Schnittstelle kann dies eine zusätzliche Wartung sein, die sich aber in der Regel lohnt.