Controllare la visibilità dei simboli

Il controllo della visibilità dei simboli può ridurre le dimensioni dell'APK, migliorare i tempi di caricamento e aiutare gli altri sviluppatori a evitare dipendenze accidentali dai dettagli di implementazione. Il modo più efficace per farlo è utilizzare gli script di versione.

Gli script di versione sono una funzionalità dei linker ELF che può essere utilizzata come forma più robusta di -fvisibility=hidden. Per una spiegazione più dettagliata, consulta la sezione Vantaggi di seguito oppure continua a leggere per scoprire come utilizzare gli script di versione nel tuo progetto.

Nella documentazione GNU a cui si fa riferimento sopra e in alcuni altri punti di questa pagina, vedrai riferimenti alle "versioni dei simboli". Questo perché l'intento originale di questi file era consentire l'esistenza di più versioni di un simbolo (di solito una funzione) in un'unica libreria per la conservazione della compatibilità con i bug nelle librerie. Android supporta anche questo utilizzo, ma in genere è utile solo per i fornitori di librerie OS e nemmeno noi li utilizziamo in Android perché targetSdkVersion offre gli stessi vantaggi con una procedura di attivazione più deliberata. Per l'argomento di questo documento, non preoccuparti di termini come "versione simbolo". Se non definisci più versioni dello stesso simbolo, "versione simbolo" è solo un raggruppamento di simboli nel file con un nome arbitrario.

Se sei l'autore di una libreria (a prescindere dal fatto che la tua interfaccia sia C/C++ o se si tratta di Java/Kotlin e il tuo codice nativo è solo un dettaglio di implementazione) piuttosto che lo sviluppatore di app, assicurati di leggere anche la pagina Consigli per i fornitori middleware.

Scrivi uno script di versione

In un caso ideale, un'app (o un file AAR) che include codice nativo conterrà esattamente una libreria condivisa, con tutte le relative dipendenze collegate in modo statico a quella libreria e l'interfaccia pubblica completa della libreria è JNI_OnLoad. In questo modo, i vantaggi descritti in questa pagina possono essere applicati nel modo più ampio possibile. In questo caso, supponendo che la raccolta sia denominata libapp.so, crea un file libapp.map.txt (il nome non deve corrispondere e il suffisso .map.txt è solo una convenzione) con i seguenti contenuti (puoi omettere i commenti):

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

Se la tua app ha più di una libreria condivisa, devi aggiungere uno script di versione per ogni libreria.

Per le librerie JNI che non utilizzano JNI_OnLoad e RegisterNatives(), puoi elencare ciascuno dei metodi JNI con i relativi nomi manipolati JNI.

Per le librerie non JNI (in genere dipendenze delle librerie JNI), dovrai enumerare l'intera API. Se la tua interfaccia è C++ anziché C, puoi utilizzare extern "C++" { ... } in uno script di versione come faresti in un file di intestazione. Ad esempio:

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

Utilizza lo script della versione durante la creazione

Lo script della versione deve essere passato al linker durante la compilazione. Segui i passaggi appropriati per il tuo sistema di compilazione riportati di seguito.

Marca

# 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.

Altro

Se il sistema di compilazione che utilizzi supporta esplicitamente gli script di versione, utilizzali.

In caso contrario, utilizza i seguenti flag del linker:

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

Il modo in cui vengono specificati dipende dal sistema di compilazione, ma in genere è presente un'opzione denominata LDFLAGS o qualcosa di simile. path/to/libapp.map.txt deve essere risolvibile dalla directory di lavoro attuale del linker. Spesso è più semplice utilizzare un percorso assoluto.

Se non utilizzi un sistema di compilazione o sei un gestore di un sistema di compilazione che vuole aggiungere il supporto per gli script di versione, questi flag devono essere passati a clang (o clang++) durante il collegamento, ma non durante la compilazione.

Vantaggi

Le dimensioni dell'APK possono essere migliorate quando si utilizza uno script di versione perché riduce al minimo l'insieme di simboli visibili in una libreria. Se viene indicato al linker esattamente quali funzioni sono accessibili agli utenti chiamanti, il linker può rimuovere dalla libreria tutto il codice non raggiungibile. Questa procedura è un tipo di eliminazione del codice inutilizzato. Il linker non può rimuovere la definizione di una funzione (o di un altro simbolo) che non è nascosta, anche se la funzione non viene mai chiamata, perché il linker deve assumere che un simbolo visibile faccia parte dell'interfaccia pubblica della libreria. Se nascondi i simboli, il linker può rimuovere le funzioni che non vengono chiamate, riducendo le dimensioni della libreria.

Le prestazioni di caricamento della libreria sono migliorate per motivi simili: le rilocazioni sono obbligatorie per i simboli visibili perché sono sovraponibili. Questo non è quasi mai il comportamento desiderato, ma è ciò che è richiesto dalla specifica ELF, quindi è l'impostazione predefinita. Tuttavia, poiché il linker non può sapere quali simboli (se presenti) intendi interporre, deve creare riallocazioni per ogni simbolo visibile. La loro eliminazione consente al linker di omettere queste rilocazioni in favore di salti diretti, il che riduce il lavoro che il linker dinamico deve svolgere durante il caricamento delle librerie.

L'enumerazione esplicita della piattaforma API impedisce anche ai consumatori delle tue librerie di dipendere erroneamente dai dettagli di implementazione della libreria, poiché questi dettagli non saranno visibili.

Confronto con le alternative

Gli script di versione offrono risultati simili ad alternative come -fvisibility=hidden o per funzione __attribute__((visibility("hidden"))). Tutti e tre gli approcci controllano quali simboli di una libreria sono visibili alle altre librerie e a dlsym.

Il principale svantaggio degli altri due approcci è che possono nascondere solo i simboli definiti nella libreria in fase di creazione. Non possono nascondere i simboli dalle dipendenze delle librerie statiche della libreria. Un caso molto comune in cui questo fa la differenza è l'utilizzo di libc++_static.a. Anche se la tua build utilizza -fvisibility=hidden, mentre i simboli della raccolta verranno nascosti, tutti i simboli inclusi da -fvisibility=hidden diventeranno simboli pubblici della raccolta.libc++_static.a Al contrario, gli script della versione offrono un controllo esplicito dell'interfaccia pubblica della libreria; se il simbolo non è elencato esplicitamente come visibile nello script della versione, verrà nascosto.

L'altra differenza può essere un vantaggio e un svantaggio allo stesso tempo: l'interfaccia pubblica della biblioteca deve essere definita esplicitamente in uno script di versione. Per le librerie JNI, questo è in realtà banale, perché l'unica interfaccia necessaria per una libreria JNI è JNI_OnLoad (poiché i metodi JNI registrati con RegisterNatives() non devono essere pubblici). Per le biblioteche con un'interfaccia pubblica di grandi dimensioni, questo può essere un carico di manutenzione aggiuntivo, ma di solito vale la pena.