シンボルの可視性を制御する

シンボルの可視性を制御すると、APK のサイズが縮小され、読み込み時間が短縮されます。また、他のデベロッパーが実装の詳細に誤って依存するのを防ぐこともできます。これを行う最も確実な方法は、バージョン スクリプトを使用することです。

バージョン スクリプトは ELF リンカーの機能であり、-fvisibility=hidden のより堅牢な形式として使用できます。詳細については、下記のメリットをご覧ください。プロジェクトでバージョン スクリプトを使用する方法については、以下をご覧ください。

上記の GNU ドキュメントや、このページの他のいくつかの場所では、「シンボル バージョン」への参照があります。これらのファイルの当初の意図は、ライブラリ内のバグ互換性を維持するために、シンボル(通常は関数)の複数のバージョンを 1 つのライブラリに存在させることだったからです。Android でもこの使用方法がサポートされていますが、通常は OS ライブラリ ベンダーにのみ使用されます。targetSdkVersion は、より慎重なオプトイン プロセスで同じメリットを提供するため、Google でも Android で使用していません。このドキュメントのトピックでは、「シンボル バージョン」のような用語については心配しないでください。同じシンボルの複数のバージョンを定義していない場合、「シンボル バージョン」は、ファイル内のシンボルの任意の名前付きグループ化にすぎません。

アプリ デベロッパーではなく、ライブラリ作成者(インターフェースが C/C++ の場合、または Java/Kotlin でネイティブ コードが実装の詳細に過ぎない場合)は、ミドルウェア ベンダー向けのアドバイスも必ずお読みください。

バージョン スクリプトを作成する

理想的なケースでは、ネイティブ コードを含むアプリ(または AAR)には、共有ライブラリが 1 つだけ含まれ、そのすべての依存関係がその 1 つのライブラリに静的にリンクされ、そのライブラリの完全な公開インターフェースが 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.
    *;
};

アプリに複数の共有ライブラリがある場合は、ライブラリごとにバージョン スクリプトを 1 つ追加する必要があります。

JNI_OnLoadRegisterNatives() を使用していない JNI ライブラリの場合は、代わりに各 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-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.

その他

使用しているビルドシステムでバージョン スクリプトが明示的にサポートされている場合は、そのスクリプトを使用します。

それ以外の場合は、次のリンカー フラグを使用します。

-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"))) などの代替手段と同様の結果が得られます。3 つのアプローチはすべて、ライブラリのどのシンボルが他のライブラリと dlsym に公開されるかを制御します。

他の 2 つのアプローチの最大の欠点は、ビルド中のライブラリで定義されたシンボルしか非表示にできないことです。ライブラリの静的ライブラリ依存関係のシンボルを非表示にすることはできません。libc++_static.a を使用する場合、この違いが非常に重要になります。ビルドで -fvisibility=hidden を使用している場合でも、ライブラリ独自のシンボルは非表示になりますが、libc++_static.a に含まれるすべてのシンボルはライブラリのパブリック シンボルになります。これに対して、バージョン スクリプトはライブラリの公開インターフェースを明示的に制御します。シンボルがバージョン スクリプトに明示的にリストされていない場合、シンボルは非表示になります。

もう一つの違いは長所と短所の両方です。ライブラリの公開インターフェースはバージョン スクリプトで明示的に定義する必要があります。JNI ライブラリで必要なインターフェースは JNI_OnLoad のみであるため、JNI ライブラリの場合、これは実質的に簡単です(RegisterNatives() に登録されている JNI メソッドは公開する必要がないため)。パブリック インターフェースが大きいライブラリでは、メンテナンスの負担が増加する可能性がありますが、通常は価値があります。