ご無沙汰しておりまする。
そもそもなんで?
Androidのアプリでパフォーマンスが必要なケース、あるいは使いたいライブラリがc/c++でできているなどの理由でNDKを使うことがあるかと思います(ゲームだったり映像処理アプリだったり)。
同時に様々な大人の事情でアプリのminSDKレベルを低く設定せざるをえないこともあるかと思います。
Java/KotlinであればたとえminSDKレベルが低くても、実行する端末のAPIレベル(OSのバージョン)が高ければ Build.VERSION.SDK_INT で条件分岐して新しいAPIを使うということが比較的簡単にできます。
一方、NDKを使ったアプリの場合には基本的にコンパイル/ビルド時のminSDKによって利用できるAPIが制限されてしまうため、たとえ実行する端末のAPIレベルが高くても通常は新しいAPIを使うことはできません。
もちろん、
__ANDROID_API__ マクロを使った条件コンパイルは可能ですが、minSDKが低ければいくつものAPIが使えないのは変わりません。
先ほど「通常は」と書きましたが、こういう場合の対処法がいくつかあります。
- 新しいAPIを使うのは諦めるm(__)m
- minSDKを高く設定して古い端末を切り捨てるm(__)m
- minSDKを変えた複数のapk/aabを生成する💦💦💦
- minSDKは変えずに動的リンクを利用して対応端末では新しいAPIを使えるようにする👍
前2つは何かを切り捨てることになりますが大人の事情で許されないことがあります。かといってminSDKを高く設定すると古い端末へインストールできなくなってしまいますし、minSDKを変えた複数のapk/arbを生成するのも管理の手間・ビルド時間が長くなるなどで不幸になるだけです。
ということで、「minSDKは変えずに動的リンクを利用して対応端末では新しいAPIを使えるようにする」と言う話です。
CPU⇔GPU間でのメモリーコピー削減により描画の高速化が期待できる(らしい)AHardwareBuffer_xxx系APIにはminSDK>=26の制限があるのですが、今回はminSDK=16のままで、実行時にAPI>=26の端末の場合にはAHardwareBuffer_xxxを使った描画の高速化ができるように動的リンクとやらをやってみます。
動的リンクってなんじゃ?
動的にリンクすることです(;^^)ヘ..🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸
Android Studio等のIDEを使ったJava/Kotlinのアプリしか作ったことのない人にはわかりにくいかもしれませんが、自分で作った部分と他の人が作った部分(ライブラリ・フレームワーク)の2つの部分を組み合わせてアプリができあがります。(なぜライブラリやフレームワークが必要かというのはとりあえず放置しておきます)
細かいところは置いといて、使おうとしているクラスや関数・メソッド等が確かに存在して使える状態にあるかを確認して結びつける処理がリンクになります。
この「リンク」をする方法として大きく次の2通りあります。
- 静的リンク
- 動的リンク
簡単に言えば「静的リンク」はアプリを作る際にリンク処理を全て終わらせる方法、「動的リンク」はアプリを作る際には対象のクラスや関数等が存在すると仮定しておいて実行時にリンクできればそれを使う方法となります。
実際には「静的リンク」であってもフレームワークのライブラリなどのように、アプリの作成時にはNDKに含まれるライブラリを使い、実行されるときには端末内に含まれるライブラリが使われようなものもあるので「静的リンク」と「動的リンクの」境目は多少曖昧ですが。
つまり動的リンクを使うように作って端末で実行する際にその関数やクラスが存在していれば、minSDKを低く設定していても新しい端末では新しいAPI、古い端末では古いAPIを使うと言うように動的に切り替えることができるのです。Java/Kotlinと同じようなことがNDKでもできるようになるのです。スバラシイ✨
もちろん新しい端末で動かすときと古い端末で動かすときに同じ処理を行うことはできませんので、代替処理を用意したり特定の機能を無効にしたり等の処理を実装する必要があります。
AHardwareBufferってなんじゃ?
Androidのシステム内には高速に描画処理を行うための仕組みがいくつかあります。
そのうちの1つに
GraphicBuffer というものがあります。
GraphicBufferは次のような機能を備えています。
- 複数のプロセス間で相互にアクセス可能
- CPU・GPU・カメラ・ハードウエアエンコーダーなどの複数のハードウエアデバイス間で共有可能
- EGLImageを生成してOpenGL|ESでのテクスチャやレンダーバッファーとして利用可能
このためGraphicBufferを使うことで、プロセス間で映像レンダリング結果を引き渡したり、CPU⇔GPU間のメモリーコピーを削減したりできます。
つまり高速で映像をレンダリングするのに効果的と言うことになります。
ただし、GraphicBufferはプライベートなAPI(ライブラリ)であり、一般のアプリが利用できるようになっておらず、端末から抜き出した共有ライブラリ(libui.so)とリンクしたり、AOSPでアプリをビルドするなど少し特殊なビルド方法が必要でした。
またAndroid7/API24以降ではlibui.so関係へのアクセスに制限がかかりシステムアプリ以外はGraphiocBuffer/libui.soを直接利用することはできなくなってしまいました(´・ω・`)
Android8/API26以降になって通常のユーザーアプリからでもGraphicBufferを利用できる仕組みとしてGraphiocBufferをラップした HardwareBuffer がAPIとして加わりました。Java/KotlinからであればHardwareBuffer、NDKからであればAHardwareBuffer_xxx関数を使ってGraphicBufferを間接的に利用できます。
Android7/API24とAndroid7.1-7.1.2/API25は完全に無かったことにされてて可哀想💦
ということでAHardwareBufferを動的リンク…新たな敵が👀
ということで、GraphicBufferの代わりにHardwareBufferを使えるようになったのですが、ここでminSDKの呪縛が…
HardwareBufferを使えるのはAndroid8/API26以降なのです。NDKからだとminSDK=26(以降)にしないとビルドすること自体ができません😱
なのでこのままでは例えば「minSDK=16でビルドしておいて、Android8/API26以降の端末で実行する場合はHardwareBufferを使う」ということがNDK自体ではできないのです。
そこでAHardwareBuffer_xxxを動的リンクして使えるようにしよう(^o^)/と言うことになります。
しかぁ〜し、NDKr23以降で新たな罠が
AHardwareBuffer_xxx関数と関連する定数や構造体はNDKに含まれる hardware_buffer.h 内で定義されています。
NDKr22までは、定数や構造体はminSDKに関わりなくアクセス可能、関数プロトタイプ宣言は __ANDROID_API__ で26以上の場合のみ定義されるようになっていました。ですのでAHardwareBuffer_xxxの関数ポインタとそれを保持する変数を宣言しておいて、アプリ実行時に libnativewindow.so と動的リンクを試みることができました。
しかぁ〜し、NDKr23から
hardware_buffer.hが改悪されてしまったのです。
なんと、関数プロトタイプ宣言の条件コンパイルが削除されてしまったのです😱
AHardwareBuffer_xxx関数の関数プロトタイプ宣言には
__INTRODUCED_IN(26) マクロが付いているので、minSDK<26ではそのままではビルドできなくなってしまったのです😡
NDKr23以降でのヘッダーファイル改悪は hardware_buffer.hに限ったことではなく gl3.h等のOpenGLES3関係や、く NdkMediaCodec.h等のMediaCodec関係も同様で、GLES3やMediaCodecも動的リンク処理の一手間掛かるようになってしまいました。
やったぜ👍
まぁだからといって動的リンクができなくなったわけではありません。ヘッダーファイル改悪によりソースコードレベルの互換性を維持するのに一手間余分に必要になっただけです。
つまり、NDKr22までであればヘッダーファイル内の公式APIの関数プロトタイプ宣言は条件コンパイルで無効化されていたので、公式APIの関数プロトタイプと同名の関数ポインタを使えました。
NDKr23以降では公式APIの関数プロトタイプがminSDKに関わらず宣言されるため、minSDK>=26では公式APIの関数(AHardwareBuffer_xxx関数)、minSDK<26の場合は自前定義したAHardwareBuffer_xxxと異なる関数名(例えばAAHardwareBuffer_xxx関数)を使うことになります。
ソースコードレベルの互換性は維持するには、minSDK>=26の場合にはdefine/typedef/usingでエリアスを宣言して自前定義と同じ関数名にすることになります。
ということでAHardwareBuffer_xxxを動的リンクするコードの一部、まずはヘッダーファイルから。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
bool init_hardware_buffer(); #include <android/hardware_buffer.h> #if __ANDROID_API__ >= 26 // libnativewindow.soへ静的リンクするとき #define AAHardwareBuffer_allocate AHardwareBuffer_allocate #define AAHardwareBuffer_acquire AHardwareBuffer_acquire #define AAHardwareBuffer_release AHardwareBuffer_release #define AAHardwareBuffer_describe AHardwareBuffer_describe #define AAHardwareBuffer_lock AHardwareBuffer_lock #define AAHardwareBuffer_unlock AHardwareBuffer_unlock #define AAHardwareBuffer_sendHandleToUnixSocket AHardwareBuffer_sendHandleToUnixSocket #define AAHardwareBuffer_recvHandleFromUnixSocket; AHardwareBuffer_recvHandleFromUnixSocket #define AAHardwareBuffer_lockPlanes AHardwareBuffer_lockPlanes #define AAHardwareBuffer_isSupported AHardwareBuffer_isSupported #define AAHardwareBuffer_lockAndGetInfo; AHardwareBuffer_lockAndGetInfo #else // libnativewindow.soへ動的リンクを試みる場合 #include <inttypes.h> #include <sys/cdefs.h> #include <android/rect.h> __BEGIN_DECLS ... typedef int (*AHardwareBuffer_allocate_ptr)(const AHardwareBuffer_Desc* desc, AHardwareBuffer** outBuffer); extern AHardwareBuffer_allocate_ptr AAHardwareBuffer_allocate; ... |
次いでコード側。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include <dlfcn.h> #include "hardware_buffer_stub.h" bool init_hardware_buffer() { ENTER(); static void *dl_handle = nullptr; if (!dl_handle) { dl_handle = dlopen("libnativewindow.so", RTLD_LAZY); if (dl_handle) { MARK("libmediandk.so found, minPI=%d", __ANDROID_API__); #define FIND_PROC(f) (f##_ptr)dlsym(dl_handle,#f) AAHardwareBuffer_allocate = FIND_PROC(AHardwareBuffer_allocate); // API>=26 AAHardwareBuffer_acquire = FIND_PROC(AHardwareBuffer_acquire); // API>=26 AAHardwareBuffer_release = FIND_PROC(AHardwareBuffer_release); // API>=26 AAHardwareBuffer_describe = FIND_PROC(AHardwareBuffer_describe); // API>=26 AAHardwareBuffer_lock = FIND_PROC(AHardwareBuffer_lock); // API>=26 AAHardwareBuffer_unlock = FIND_PROC(AHardwareBuffer_unlock); // API>=26 AAHardwareBuffer_sendHandleToUnixSocket = FIND_PROC(AHardwareBuffer_sendHandleToUnixSocket); // API>=26 AAHardwareBuffer_recvHandleFromUnixSocket = FIND_PROC(AHardwareBuffer_recvHandleFromUnixSocket); // API>=26 AAHardwareBuffer_lockPlanes = FIND_PROC(AHardwareBuffer_lockPlanes); // API>=29 AAHardwareBuffer_isSupported = FIND_PROC(AHardwareBuffer_isSupported); // API>=29 AAHardwareBuffer_lockAndGetInfo = FIND_PROC(AHardwareBuffer_lockAndGetInfo); // API>=29 #undef FIND_PROC } } const bool supported = dl_handle && AAHardwareBuffer_allocate && AAHardwareBuffer_acquire && AAHardwareBuffer_release && AAHardwareBuffer_describe && AAHardwareBuffer_lock && AAHardwareBuffer_unlock && AAHardwareBuffer_sendHandleToUnixSocket && AAHardwareBuffer_recvHandleFromUnixSocket; LOGD("supported=%d", supported); RETURN(supported, bool); } ... AHardwareBuffer_allocate_ptr AAHardwareBuffer_allocate = nullptr; ... |
てな感じです。
AAHardwareBuffer_xxx関数を使う場合には、最初に init_hardware_buffer 関数を呼び出してtrueが返ればAAHardwareBuffer_xxx関数ポインタを動的リンクで呼び出し可能と言うことになります。
AHardwareBufferの効果はいかに?
Android端末へwebカメラを繋いで映像を取得したり、webカメラ/内蔵カメラからの映像にOpenCV等で何らかの処理を行ったりする際には、CPU側とGPU側とで頻繁にデータのやりとりを行う必要があります。
従来だとCPU側からGPU側へのコピーだとglTexImage2D関数やglTexSubImage2D関数、GPU側からCPU側へのコピーだとglReadPixels関数を使うことが多いと思いますが、AHardwareBufferを使うことでどれぐらい改善されるのでしょうか?
結果は次回へ(^.^)/~~~