Orientações para fornecedores de middleware

A distribuição de middleware criado com o NDK gera outros problemas com os quais os desenvolvedores de apps não precisam se preocupar. As bibliotecas pré-compiladas impõem aos usuários algumas das opções de implementação delas.

Como escolher níveis de API e versões do NDK

Seus usuários não podem usar uma minSdkVersion anterior à usada por você. Se os apps dos usuários precisam ser executados na API de nível 21, não é possível criar para a de nível 24. Não há problema em criar uma biblioteca para um nível de API anterior ao dos usuários. Você pode criar para a API de nível 16 e manter a compatibilidade com os usuários de nível 21.

As versões do NDK são amplamente compatíveis umas com as outras, mas às vezes ocorrem mudanças que corrompem a compatibilidade. Se você sabe que todos os seus usuários estão usando a mesma versão do NDK, é melhor usar essa versão. Caso contrário, use a versão mais recente.

Como usar a STL

Se você estiver programando em C++ e usando a STL, a escolha entre libc++_shared e libc++_static afetará seus usuários caso você distribua uma biblioteca compartilhada. Se esse for o caso, você precisará usar libc++_shared ou garantir que os símbolos libc++ não sejam expostos pela biblioteca. A melhor maneira de fazer isso é declarando explicitamente a superfície da ABI com um script de versão. Isso também ajuda a manter os detalhes de implementação particulares. Por exemplo, uma biblioteca aritmética simples pode ter o seguinte script de versão:

LIBMYMATH {
global:
    add;
    sub;
    mul;
    div;
    # C++ symbols in an extern block will be mangled automatically. See
    # https://stackoverflow.com/a/21845178/632035 for more examples.
    extern "C++" {
        "pow(int, int)";
    }
local:
    *;
};

Um script de versão precisa ser a opção preferencial porque é a maneira mais robusta de controlar a visibilidade do símbolo. Essa é uma prática recomendada para todas as bibliotecas compartilhadas, middleware ou não, porque impede que os detalhes de implementação sejam expostos e melhora o tempo de carregamento.

Outra opção menos robusta é usar -Wl,--exclude-libs,libc++_static.a -Wl,--exclude-libs,libc++abi.a na vinculação. Ela é menos robusta porque oculta apenas os símbolos nas bibliotecas com nome explícito e nenhum diagnóstico é relatado para bibliotecas que não são usadas. Um erro de digitação no nome da biblioteca não é um erro, e fica a cargo do usuário manter a lista de bibliotecas atualizada. Essa abordagem também não oculta os detalhes de implementação.

Como distribuir bibliotecas nativas em AARs

O Plug-in do Android para Gradle pode importar dependências nativas distribuídas em AARs. Se os usuários estiverem usando o Plug-in do Android para Gradle (AGP, na sigla em inglês), essa será a maneira mais fácil para eles consumirem sua biblioteca.

As bibliotecas nativas podem ser empacotadas em um AAR pelo AGP. Essa será a opção mais fácil caso sua biblioteca já tenha sido criada pelo externalNativeBuild.

Os builds que não são do AGP podem usar ndkports ou executar empacotamento manual seguindo a documentação da Prefab (link em inglês) para criar o subdiretório prefab/ do AAR.

Middleware Java com bibliotecas JNI

As bibliotecas Java que incluem bibliotecas JNI (ou seja, AARs que contêm jniLibs) precisam ter cuidado para que as bibliotecas JNI inclusas não entrem em conflito com outras bibliotecas no app do usuário. Por exemplo, se o AAR incluir libc++_shared.so, mas for uma versão diferente da usada pelo app, apenas uma será instalada no APK e poderá levar a um comportamento não confiável.

A solução mais confiável é as bibliotecas Java não incluírem mais do que uma biblioteca JNI. Essa também é uma boa recomendação para apps. Todas as dependências, incluindo a STL, serão vinculadas estaticamente à biblioteca de implementação, e um script de versão precisará ser usado para aplicar a plataforma da ABI. Por exemplo, uma biblioteca Java com.example.foo que inclua a biblioteca JNI libfooimpl.so precisará usar o seguinte script de versão:

LIBFOOIMPL {
global:
    JNI_OnLoad;
local:
    *;
};

Esse exemplo usa registerNatives via JNI_OnLoad, conforme descrito em Dicas de JNI para garantir que a plataforma de ABI mínima seja exposta e o tempo de carregamento da biblioteca seja minimizado.