iOSでのCGContextDrawImage()を最適化する

どうも、吉村です。

今回はiOSにおけるCoreGraphicsの話題でなおかつCGContextDrawImage(), およびCGImageRefにフォーカスした非常にピンポイントな話題です。

CGContextDrawImageが遅い!
そう感じたのは以下のような状況です。

「CGContextDrawImage()を1秒間に何十回もコールする」

主にリアルタイムグラフィックスを扱う場合ですね。

え?そんなに?
と意外かもしれませんが、CGContextDrawImage()は、
ゲーム等を作る場合以外では意外と高いレートでコールされることはないので、気づいている人は少ないかもしれません。

では何が遅いのか?

それは

「画像をラスターデータに展開する処理」

です。

一般的な画像形式は基本的に圧縮されています。
例えばpngは、Deflateというアルゴリズムを使うzlibにより圧縮されています。

なので、実際にコンピュータに読み込まれ、処理され、画面に出すためには、
必ずどこかでラスターデータ、つまりピクセルデータの二次元配列に変換しなければなりません。

もう薄々気づいている方もいるかもしれませんが、
密な描画ループで大きなオーバーヘッドを引き起こすその原因は、

「UIImage等で普通に読み込んだCGImageRefは通常圧縮されたままであり、
CGContextDrawImage()は描画する瞬間になって初めてラスター展開する」

という挙動によるものです。
実際CGContextDrawImage()を高いレートで呼ぶことのほうが少ないので、
この挙動はメモリを節約する非常に良い挙動だと思います。

さて、ではどうすればこのオーバーヘッドを削ることが出来るでしょうか?
答えは一つです。
あらかじめラスター展開しちゃえ!

ということで、ラスター展開されたCGImageRefを作成するコードをまず探してみると、
おっと、丁度良いものが見つかりました。
http://www.benmcdowell.com/blog/2012/01/12/speeding-up-cgcontextdrawimage-calls-in-ios-4/

このサイトのコードをちょちょっといじくり、以下のようにしました。

これでオッケー!
と言いたいところですが、このコードはまだ最適化の余地があります。
処理を追いかけてみると、

1、CGBitmapContextCreateでビットマップコンテキストを生成
この箇所でまずラスターデータがwidth x height x 4 バイト分裏で確保されています。

2、CGContextDrawImageでラスター展開を行いながら、ビットマップコンテキストに画像データを移動

3、CGBitmapContextCreateImage()により、コンテキストからCGImageRefを生成します。

という処理になります。
なので、1で一度ラスターデータを確保して、3で再び確保してコピーしているのです。
これは非常に無駄です。

なので、以上をふまえて、さらに最適化したコードは以下の様になります。

このコードでは

1、ラスター用のメモリを確保する

2、ラスター用のメモリを使ったビットマップコンテキストを作成する
この場合、裏でメモリは確保されず、こちら側で用意したメモリが使われます。

3、CGContextDrawImageでラスター展開を行いながら、ラスター用のメモリに画像データを移動

4、確保していたラスターデータから直接CGImageRefを生成する

という流れになり、
ラスターデータのコピーを避けることができていることが分かると思います。

非常に優れたiOS SDKですが、
このようなちょっとしたことで大きくパフォーマンスが変わることは往々にしてあるので、
注意深く見ていきたいですね。
特に高レベルなAPIを使うとなかなか気づきにくいものです。