Understanding Android Surface

以下の話題についてより包括的な理解をしたい人は Graphics architecture を参照されたし。


Surface というクラスがある。この名前は完全に間違っていて、実際は ImageQueue みたいな名前だと思えばよい。ここでいう Image は、たとえば GPU が処理したりできる、ART の外で確保された、プロセスを跨いでやり取りされる画像のための共有メモリである。ではなぜ Surface という名前なのか、と考えるのは時間の無駄なうえに精神衛生を危険に晒すので、十分な資産を形成し引退するまではやめておいた方がよい。

Producer / Consumer

Android のサブシステムには画像 (Image) を生成するコンポーネントと Image を消費する(受け取って処理する)コンポーネントがある。

Image を生成する Producer side には以下のようなものがある:

  • MediaCodec: 圧縮された動画ファイルをデコードし、動画のフレーム画像を Image として produce する。
  • Camera: カメラがセンサーから吸い上げた画像をproduce する。動画として次々と生成することも、写真の撮影リクエストごとにひとつの image を生成することもできる。
  • OpenGL: eglSwapBuffers() のタイミングで GL の描画結果 Image をproduce する。

これら producer side のサブコンポーネントは結果の画像を書き込む ImageQueue すなわち Surface を受け取る。例: MediaCodec#configure()

画像を受け取って処理する consumer side には以下のようなものがある:

  • ふたたび MediaCodec. 流れ込んできた画像列を動画として圧縮する。
  • SurfaceView: 流れ込んできた画像を View の一部として画面に表示する。
  • SurfaceTexture: 流れ込んできた画像を OpenGL の texture に割り当てる。

これら consumer side コンポーネントは、画像を受け取るための ImageQueue すなわち Surface を公開している。例: SurfaceView#getHolder().getSurface(). SurfaceTexture はちらっとドキュメントを眺めただけだと Surface を取り出せないようにみえるが、Surface のコンストラクタが SurfaceTexture を引数にうけとるという形で公開している。なぜそんな API デザインなのかは、やはり考えるだけ時間の無駄である(e.g. 単にゴミなだけ。)

Operating On Pixels

Surface の上を流れる画像にアプリケーションからアクセス、加工する代表的な手段は OpenGL である。すなわち画像を SurfaceTexture 経由で GL texture にマップし、GL でレンダリングがてら pixel shader などでなんかする。

GPU でなく CPU で処理したい人には ImageReader というクラスが用意されている。これは ImageQueue すなわち Surface の consumer side コンポーネントの一つ。そのコンポーネントが自ら画像をなんかするかわりに、Java のレイヤに Image というオブジェクトをとりだす API がある。この Image オブジェクトを使うと ByteBuffer ごしに画像のピクセルを読み書きできる。

CPU で処理したいユースケースの代表格はファイルへの書き出し。たとえばカメラの画像を JPEG で保存するとか。

ImageReader で読み出した画像は用が済んだら close() のうえ捨ててもいいし、ImageWriter オブジェクトを使って他の ImagQueue すなわち Surface にそのイメージを enqueue することもできる。ImageWriter は ImageReader の対, producer side のクラスである。

Image#close() するのはかったるいが、Image の実体は Java heap でなく希少な共有メモリ・そのうえサイズがでかいので、こまめに開放しないとすぐ枯渇してしまう。やむなし。

SurfaceView

SurfaceView は ImageQueue/Surface の consumer である。ではどうやってその image を consume し、画面に表示するのだろうか。

すべての Activity は最低一つの SurfaceView を持っている。以下ではこれを Root SurfaceView と呼ぶ。Root SurfaceView には持ち主のアプリが描画される。アプリが描画した画面の Image を実際に消化するのは SurfaceFlinger と呼ばれるプロセスである。これは複数のアプリの描画結果をくみあわせ最終的な画面を描画するプロセスで、他の OS だとWindow Manager とか Window System などと呼ばれるものの機能の一部である。SurfaceFinger は最終的な画面を OpenGL で描くこともあるし、可能ならより省電力で 2D 専用の composition hardware を使う。

つまりアプリの描画結果はアプリ自体のプロセスから ImageQueue/Surface を介して SurfaceFlinger に送られる。SurfaceView は画像だけでなく画像の表示場所や大きさすなわちレイアウト結果も SurfaceFlinger に伝える。(こうした位置情報の伝達は Surface と独立した API/Binder をたどる。) 

Root SurfaceView ではなく、アプリケーションが確保した SurfaceView も基本的には同じように振る舞う。すなわち、受け取った画像はレイアウト結果ともども SurfaceFlinger に送られる。

別の言い方をすると、アプリは SurfaceView をレイアウトはすれど描画はしない。たとえば OpenGLで SurfaceView に何か書き込んでもアプリ自身の描画結果には反映されない。Android が内部で確保したアプリの Root SurfaceView とアプリのコードが確保した SurfaceView はそれぞれ独立に描画され、それぞれの Image を SurfaceFlinger に送る。SurfaceFlinger はそれを重ね合わせ、辻褄のあった最終的な画面を描画する。

プロセスをまたぎ最終的な画面画像の合成プロセス (composition という) に介入する荒々しさを理解していないと, SurfaceView はすごく奇妙な振る舞いをするように見える。そういう意味で SurfaceView という名前は misleading とは言わないまでももうちょっとそういう事情を反映した名前のほうがよかった。たとえば CompositionView とか。

ところで Android は Camera や VideoCodec にも独立したプロセスがある。こうしたコンポーネントはそのプロセス内で Image を生成し、ImageQueue/Surface を介してアプリに届ける。しかし SurfaceView の実際の consumer は SurfaceFlinger プロセスなので、VideoCodec や Camera を SurfaceView に繋ぐと Image はアプリのプロセスをかすりもせず SurfaceFlinger に届く。なのでアプリの UI thread が多少もたついてもビューファインダやビデオはコマ落ちせずに再生される。この低遅延な振る舞いは SurfaceView の大きな利点である。

TextureView

TextureView は SurfaceView と同じく Surface すなわち ImageQueue を介して画像を受け取る consumer である。しかし SurfaceView と異なり、TextureView は実際にアプリのプロセスが描画する。

各アプリには RenderThread  と呼ばれるスレッドがあり、このスレッドは UI thread の作ったディスプレイリストを OpenGL で描画する。 TextureView は RenderThread に SurfaceTexture を作って画像を受けとり、RenderThread が描画する画面の一部としてそのテクスチャを描画する。これは LAYER_TYPE_HARDWARE を指定した View の振る舞いとよく似ているが、RenderThread がテクスチャの中身を描くかわりに Surface の向こうの producer からテクスチャの中身が届く点が異なる。

TextureView は普通の View と同じく RenderThread で描画されるため、レイアウトとかでややこしいことにならない。だから扱いやすい。一方で一回余計な GL の描画を挟むぶん 1 フレーム遅延があるし、電力効率も悪いし、UI thread がもたつくと巻き込まれる。利便性と性能をトレードオフしている。

TextureView も名前が悪いね。SurfaceView とは逆に実装の詳細を晒しすぎている。LayerView とか呼べばいいのに。

Pipeline

どこかの producer から 受け取った Image のストリームをどのような方法で画面に描くか、画面に書く前の加工をどうするか。Surface を中心とした一連の画像処理を pipeline と呼ぶことがある。Pipeline のデザインはメディア系アプリの性能や安定性に大きく影響する。同時に HAL や OS の都合で思わぬ制限も多い。実験しつつ良いデザインを探求する価値がある。が、世の中は割とコピペと cargo cult に支配されがちに見える。ここに書いたシステムのデザインの意図みたいのは理解しておくと少しマシな判断ができる、かもしれない。(できないかもしれない。)