新しい API の使用

このページでは、古いデバイスとの互換性を維持しながら、新しいバージョンの OS で実行する際に、アプリが新しい OS の機能を使用する方法について説明します。

デフォルトでは、アプリ内の NDK API への参照は、強い参照になっています。Android のダイナミック ローダは、ライブラリが読み込まれると積極的に問題を解決します。シンボルが見つからない場合、アプリは中止されます。これは Java の動作に反しており、存在しない API が呼び出されるまで例外はスローされません。

そのため、アプリの minSdkVersion よりも新しい API への強い参照は、NDK が作成できません。これにより、テスト中には機能しても、古いデバイスでは読み込みに失敗する(UnsatisfiedLinkErrorSystem.loadLibrary() からスローされる)コードを誤って配布するのを防ぐことができます。一方、アプリの minSdkVersion よりも新しい API を使用するコードを記述するのは難しくなります。これは、通常の関数呼び出しではなく、dlopen()dlsym() を使用して API を呼び出す必要があるためです。

強い参照の代わりに、弱い参照を使用する方法もあります。ライブラリの読み込み時に弱い参照が見つからない場合、そのシンボルのアドレスは nullptr に設定されます。読み込みが失敗することはありません。これらの関数を安全に呼び出すことはできませんが、コールサイトが利用できないときに API を呼び出せないように保護されている限り、コードの残りの部分は実行でき、dlopen()dlsym() を使用しなくても通常どおりに API を呼び出すことができます。

弱い API リファレンスはダイナミック リンカーからの追加サポートを必要としないため、どのバージョンの Android でも使用できます。

ビルドで弱い API 参照を有効にする

CMake

CMake の実行時に -DANDROID_WEAK_API_DEFS=ON を渡します。externalNativeBuild から CMake を使用している場合は、build.gradle.kts に以下を追加します(まだ build.gradle を使用している場合は Groovy の同等の関数)。

android {
    // Other config...

    defaultConfig {
        // Other config...

        externalNativeBuild {
            cmake {
                arguments.add("-DANDROID_WEAK_API_DEFS=ON")
                // Other config...
            }
        }
    }
}

ndk-build

次のコードを Application.mk ファイルに追加します。

APP_WEAK_API_DEFS := true

Application.mk ファイルがまだない場合は、Android.mk ファイルと同じディレクトリに作成します。ndk-build では、build.gradle.kts(または build.gradle)ファイルに追加の変更は必要ありません。

他のビルドシステム

CMake や ndk-build を使用していない場合は、ビルドシステムのドキュメントを参照して、この機能を有効にするおすすめの方法があるかどうかを確認してください。ビルドシステムがこのオプションをネイティブにサポートしていない場合は、コンパイル時に次のフラグを渡すことでこの機能を有効にできます。

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

まず、弱参照を許可するように NDK ヘッダーを構成します。2 つ目の方法では、安全でない API 呼び出しの警告をエラーに変更します。

詳しくは、ビルドシステム メンテナンス ガイドをご覧ください。

保護された API 呼び出し

この機能では、新しい API の呼び出しが魔法のように安全に行われるわけではありません。読み込み時エラーを呼び出し時エラーに延期するだけです。その利点は、実行時にその呼び出しを保護し、適切にフォールバックできることです。代替の実装を使用するか、アプリのその機能がデバイスで利用できないことをユーザーに通知するか、そのコードパスを完全に回避します。

アプリの minSdkVersion で使用できない API に対して保護されていない呼び出しを行うと、Clang が警告(unguarded-availability)を出力することがあります。ndk-build または CMake ツールチェーン ファイルを使用している場合、この警告は自動的に有効になり、この機能を有効にするとエラーになります。

dlopen()dlsym() を使用して、この機能を有効にせずに API を条件付きで使用するコードの例を次に示します。

void LogImageDecoderResult(int result) {
    void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
    CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
    auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
        dlsym(lib, "AImageDecoder_resultToString")
    );
    if (func == nullptr) {
        LOG(INFO) << "cannot stringify result: " << result;
    } else {
        LOG(INFO) << func(result);
    }
}

読みづらく、関数名が重複しており(また、C でシグネチャも記述している場合は)正常にビルドされますが、dlsym に渡される関数名を誤って入力してしまった場合は、実行時に必ずフォールバックが行われます。すべての API でこのパターンを使用する必要があります。

弱い API 参照を使用すると、上記の関数は次のように書き換えることができます。

void LogImageDecoderResult(int result) {
    if (__builtin_available(android 31, *)) {
        LOG(INFO) << AImageDecoder_resultToString(result);
    } else {
        LOG(INFO) << "cannot stringify result: " << result;
    }
}

内部で、__builtin_available(android 31, *)android_get_device_api_level() を呼び出して結果をキャッシュに保存し、31AImageDecoder_resultToString() を導入した API レベル)と比較します。

__builtin_available に使用する値を決定する最も簡単な方法は、ガード(または __builtin_available(android 1, *) のガード)なしでビルドを試し、エラー メッセージに表示されるとおりに実行することです。たとえば、minSdkVersion 24 を使用して AImageDecoder_createFromAAsset() へのガードなし呼び出しを行うと、次のようになります。

error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]

この場合、呼び出しは __builtin_available(android 30, *) によって保護される必要があります。ビルドエラーがなければ、API が常に minSdkVersion で使用可能でガードが不要であるか、ビルドが正しく構成されておらず unguarded-availability 警告が無効になっています。

NDK API リファレンスには、各 API の「API 30 で導入」という記載があります。このテキストがない場合は、サポートされているすべての API レベルで API を使用できることを意味します。

API ガードの繰り返しの回避

これを使用している場合、おそらくアプリ内には、十分な数の新しいデバイスでしか使用できないコード セクションが存在することになります。関数ごとに __builtin_available() チェックを繰り返すのではなく、特定の API レベルを必要とするものとして独自のコードにアノテーションを付けることができます。たとえば、ImageDecoder API 自体は API 30 で追加されたため、これらの API を多用する関数では、次のようにすることができます。

#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)

void DecodeImageWithImageDecoder() REQUIRES_API(30) {
    // Call any APIs that were introduced in API 30 or newer without guards.
}

void DecodeImageFallback() {
    // Pay the overhead to call the Java APIs via JNI, or use third-party image
    // decoding libraries.
}

void DecodeImage() {
    if (API_AT_LEAST(30)) {
        DecodeImageWithImageDecoder();
    } else {
        DecodeImageFallback();
    }
}

API ガードの特徴

Clang は、__builtin_available の使用方法に非常に特殊化しています。マクロで置き換えられる場合もありますが、リテラルの if (__builtin_available(...)) のみが機能します。if (!__builtin_available(...)) のような簡単なオペレーションでさえ機能しません(Clang は unsupported-availability-guard 警告と unguarded-availability を出力します)。これは Clang の将来のバージョンで改善される可能性があります。詳しくは、LLVM の不具合 33161 をご覧ください。

unguarded-availability のチェックは、使用されている関数スコープにのみ適用されます。Clang は、API 呼び出しを含む関数が保護されたスコープ内からのみ呼び出される場合でも、警告を出力します。独自のコードでガードの繰り返しを回避するには、API ガードの繰り返しを回避するをご覧ください。

これがデフォルトではないのはなぜですか?

強力な API 参照と弱い API 参照の違いは、適切に使用しない限り、前者は素早く明らかに失敗するのに対し、後者は不足している API が呼び出される原因となる操作をユーザーが行うまで失敗しないという点です。この場合、エラー メッセージは明確なコンパイル時の「AFoo_bar() is not available」エラーではなく、セグメンテーション違反になります。強い参照を使用すると、エラー メッセージが大幅に明確になり、フェイル ファスト(フェイル ファスト)がより安全なデフォルト設定になります。

これは新機能であるため、この動作を安全に処理するための既存のコードはごく少数です。Android を念頭に置いていないサードパーティのコードでも、この問題が常に発生する可能性が高いため、現時点ではデフォルトの動作を変更する予定はありません。

この方法はおすすめしますが、問題の検出とデバッグが困難になるため、知らないうちに動作が変化するのではなく、リスクを慎重に受け入れる必要があります。

注意点

この機能はほとんどの API で機能しますが、機能しない場合もあります。

最も問題が発生する可能性が最も低いのは、新しい libc API です。他の Android API と異なり、これらは __INTRODUCED_IN(X) だけでなく、ヘッダー内の #if __ANDROID_API__ >= X で保護されるため、脆弱な宣言さえも認識されません。最新の NDK がサポートする最も古い API レベルは r21 であるため、最もよく使用される libc API はすでに利用できます。新しい libc API はリリースごとに追加されます(status.md を参照)。ただし、それらが新しいほど、ほとんどのデベロッパーが必要としないエッジケースになる可能性が高くなります。ただし、これらのデベロッパーの方は、minSdkVersion が API より古い場合は、引き続き dlsym() を使用して API を呼び出す必要があります。これは解決可能な問題ですが、これを行うと、すべてのアプリでソースの互換性が損なわれるリスクが伴います(libc API の polyfill を含むコードは、libc 宣言とローカル宣言で availability 属性が一致しないためコンパイルに失敗します)。そのため、修正するかどうか、またいつ修正するかは不明です。

新しい API を含むライブラリminSdkVersion よりも新しい場合は、デベロッパーの数が増える可能性があります。この機能は、弱いシンボル参照のみを有効にします。弱いライブラリ参照というものはありません。たとえば、minSdkVersion が 24 の場合、libvulkan.so をリンクして、vkBindBufferMemory2 に対して保護された呼び出しを行うことができます。libvulkan.so は API 24 以降のデバイスで使用できるためです。一方、minSdkVersion が 23 の場合は、dlopendlsym にフォールバックする必要があります。これは、API 23 のみをサポートするデバイスではライブラリは存在しないためです。この問題を解決するための適切な解決策はありませんが、新しい API で新しいライブラリを作成できなくなるため、長期的には自然に解決します。

図書館の著者向け

Android アプリで使用するライブラリを開発する場合、この機能を公開ヘッダーで使用しないでください。行外のコードでも安全に使用できますが、インライン関数やテンプレート定義など、ヘッダー内のコードで __builtin_available に依存すると、すべてのユーザーに対してこの機能を有効にせざるを得なくなります。NDK でこの機能がデフォルトで有効になっていないのと同じ理由から、ユーザーに代わってそのような選択を行うことは避けてください。

公開ヘッダーでこの動作を必要とする場合は、機能を有効にする必要があることと、有効にするリスクを認識する旨を文書化してください。