Controlar a visibilidade do símbolo

Controlar a visibilidade do símbolo pode reduzir o tamanho do APK, melhorar os tempos de carregamento e ajudar outros desenvolvedores a evitar dependências acidentais em detalhes de implementação. A maneira mais robusta de fazer isso é com scripts de versão.

Os scripts de versão são um recurso de vinculadores ELF que pode ser usado como uma forma mais robusta de -fvisibility=hidden. Consulte a seção Benefícios abaixo para uma explicação mais detalhada ou continue lendo para saber como usar scripts de versão no seu projeto.

Na documentação do GNU (link em inglês) acima e em outros lugares nesta página, você verá referências a "versões de símbolo". Isso ocorre porque a intenção original desses arquivos era permitir que várias versões de um símbolo (geralmente uma função) existissem em uma única biblioteca para preservar a compatibilidade com bugs nas bibliotecas. O Android também oferece suporte a esse uso, mas ele geralmente é útil apenas para fornecedores de bibliotecas do SO. Nós nem as usamos no Android porque targetSdkVersion oferece os mesmos benefícios com um processo de ativação mais deliberado. Neste documento, não se preocupe com termos como "versão de símbolo". Se você não estiver definindo várias versões do mesmo símbolo, "versão do símbolo" é apenas um agrupamento de símbolos nomeado de forma arbitrária no arquivo.

Se você é um autor de biblioteca (seja a interface C/C++ ou Java/Kotlin e seu código nativo é apenas um detalhe de implementação) em vez de um desenvolvedor de apps, leia também Conselhos para fornecedores de middleware.

Escrever um script de versão

No caso ideal, um app (ou AAR) que inclui código nativo vai conter exatamente uma biblioteca compartilhada, com todas as dependências vinculadas estaticamente a essa biblioteca, e a interface pública completa dessa biblioteca é JNI_OnLoad. Isso permite que os benefícios descritos nesta página sejam aplicados da forma mais ampla possível. Nesse caso, supondo que a biblioteca seja chamada libapp.so, crie um arquivo libapp.map.txt (o nome não precisa ser igual, e o sufixo .map.txt é apenas uma convenção) com o seguinte conteúdo (você pode omitir os comentários):

# 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 o app tiver mais de uma biblioteca compartilhada, adicione um script de versão por biblioteca.

Para bibliotecas JNI que não estão usando JNI_OnLoad e RegisterNatives(), você pode listar cada um dos métodos JNI com os nomes JNI modificados.

Para bibliotecas não JNI (dependências das bibliotecas JNI, normalmente), você vai precisar enumerar a superfície completa da API. Se sua interface for C++ em vez de C, você poderá usar extern "C++" { ... } em um script de versão da mesma forma que faria em um arquivo de cabeçalho. Exemplo:

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

Usar o script de versão ao criar

O script da versão precisa ser passado para o vinculador durante a criação. Siga as etapas adequadas para seu sistema de build abaixo.

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.

Outro

Se o sistema de build que você está usando tiver suporte explícito para scripts de versão, use esse.

Caso contrário, use as seguintes flags de vinculador:

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

A forma como elas são especificadas depende do seu sistema de build, mas normalmente há uma opção chamada LDFLAGS ou algo semelhante. O path/to/libapp.map.txt precisa ser resolvido no diretório de trabalho atual do vinculador. Muitas vezes, é mais fácil usar um caminho absoluto.

Se você não estiver usando um sistema de build ou for um administrador de sistema de build que quer adicionar suporte a scripts de versão, essas flags precisam ser transmitidas para clang (ou clang++) ao vincular, mas não ao compilar.

Vantagens

O tamanho do APK pode ser melhorado ao usar um script de versão porque ele minimiza o conjunto visível de símbolos em uma biblioteca. Ao informar ao vinculador exatamente quais funções podem ser acessadas pelos autores da chamada, ele pode remover todo o código inacessível da biblioteca. Esse processo é um tipo de eliminação de códigos inativos. O linker não pode remover a definição de função (ou outro símbolo) que não está escondida, mesmo que a função nunca seja chamada, porque o linker precisa presumir que um símbolo visível faz parte da interface pública da biblioteca. Ocultar símbolos permite que o vinculador remova funções que não são chamadas, reduzindo o tamanho da biblioteca.

O desempenho de carregamento da biblioteca foi melhorado por motivos semelhantes: as relocações são necessárias para símbolos visíveis porque eles são intercombináveis. Esse comportamento quase nunca é o desejado, mas é o que é necessário de acordo com a especificação ELF, então é o padrão. No entanto, como o vinculador não pode saber quais símbolos (se houver) você pretende intercalar, ele precisa criar realocações para cada símbolo visível. Ocultar esses símbolos permite que o vinculador omita essas realocações em favor de saltos diretos, o que reduz a quantidade de trabalho que o vinculador dinâmico precisa fazer ao carregar bibliotecas.

A enumeração explícita da superfície da API também impede que os consumidores das bibliotecas dependam por engano de detalhes de implementação da biblioteca, já que esses detalhes não estarão visíveis.

Comparação com alternativas

Os scripts de versão oferecem resultados semelhantes, como alternativas como -fvisibility=hidden ou __attribute__((visibility("hidden"))) por função. As três abordagens controlam quais símbolos de uma biblioteca são visíveis para outras bibliotecas e para dlsym.

A maior desvantagem das outras duas abordagens é que elas só podem ocultar símbolos definidos na biblioteca que está sendo criada. Elas não podem ocultar símbolos de dependências estáticas da biblioteca. Um caso muito comum em que isso faz uma diferença é ao usar libc++_static.a. Mesmo que o build use -fvisibility=hidden, os símbolos da biblioteca serão ocultos, mas todos os símbolos incluídos em libc++_static.a vão se tornar públicos na biblioteca. Por outro lado, os scripts de versão oferecem controle explícito da interface pública da biblioteca. Se o símbolo não estiver explicitamente listado como visível no script de versão, ele ficará oculto.

A outra diferença pode ser um ponto positivo ou negativo: a interface pública da biblioteca precisa ser definida explicitamente em um script de versão. Para bibliotecas JNI, isso é trivial, porque a única interface necessária para uma biblioteca JNI é JNI_OnLoad (porque os métodos JNI registrados com RegisterNatives() não precisam ser públicos). Para bibliotecas com uma grande interface pública, isso pode ser uma carga de manutenção extra, mas que geralmente vale a pena.