新しい API の使用

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

デフォルトでは、アプリ内の NDK API への参照は強参照です。Android の動的ローダは、ライブラリの読み込み時にそれらをエアリース解決します。シンボルが見つからない場合、アプリは中止されます。これは、不足している API が呼び出されるまで例外がスローされない Java の動作とは対照的です。

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

強参照の代わりに弱参照を使用することもできます。ライブラリの読み込み時に弱い参照が見つからない場合、そのシンボルのアドレスは読み込みに失敗するのではなく nullptr に設定されます。それでも安全に呼び出すことはできませんが、API が使用できないときに 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

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

詳細については、Build System Maintainers Guide をご覧ください。

ガードされた 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 24AImageDecoder_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 のチェックは、使用されている関数スコープにのみ適用されます。API 呼び出しを含む関数がガードされたスコープ内からのみ呼び出される場合でも、Clang は警告を出力します。独自のコードでガードが重複しないようにするには、API ガードが重複しないようにするをご覧ください。

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

正しく使用されていない場合、強力な API 参照と弱い API 参照の違いは、前者はすぐに明らかなエラーが発生するのに対し、後者は不足している API が呼び出されるアクションをユーザーが実行するまでエラーが発生しないことです。この場合、エラー メッセージはコンパイル時の明確な「AFoo_bar() は使用できません」エラーではなく、segfault になります。強参照を使用すると、エラー メッセージが明確になり、fail-fast がより安全なデフォルトになります。

これは新しい機能であるため、この動作を安全に処理するように記述された既存のコードはほとんどありません。Android を念頭に置いて記述されていないサードパーティ コードでは、この問題が常に発生する可能性があります。そのため、現在のところ、デフォルトの動作を変更する予定はありません。

これを使用することをおすすめしますが、問題の検出とデバッグが難しくなるため、動作が知らないうちに変更されるのではなく、これらのリスクを認識したうえで受け入れる必要があります。

注意点

この機能はほとんどの API で動作しますが、動作しないケースもあります。

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

多くのデベロッパーが遭遇する可能性が高いのは、新しい API を含むライブラリminSdkVersion よりも新しい場合です。この機能では、弱いシンボル参照のみが有効になります。弱いライブラリ参照はありません。たとえば、minSdkVersion が 24 の場合、libvulkan.so は API 24 以降のデバイスで使用できるため、libvulkan.so をリンクして vkBindBufferMemory2 をガード付きで呼び出すことができます。一方、minSdkVersion が 23 の場合、API 23 のみをサポートするデバイスではライブラリがデバイスに存在しないため、dlopendlsym にフォールバックする必要があります。このケースを解決するための適切な解決策は見つかりませんが、(可能な限り)新しい API による新しいライブラリの作成が許可されなくなるため、長期的には解決するでしょう。

ライブラリ デベロッパー向け

Android アプリで使用するライブラリを開発している場合は、公開ヘッダーでこの機能を使用しないでください。アウトオブライン コードで安全に使用できますが、ヘッダー内のコード(インライン関数やテンプレート定義など)で __builtin_available に依存している場合、すべてのコンシューマがこの機能を有効にする必要があります。NDK でこの機能をデフォルトで有効にしない理由と同じ理由で、コンシューマに代わってこの選択を行うことは避けてください。

公開ヘッダーでこの動作が必要な場合は、そのことを必ずドキュメントに記載して、ユーザーがこの機能を有効にする必要があることと、そのリスクを認識できるようにしてください。