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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
static void bufferFree(void *info, const void *data, size_t size) { free((void *)data); } + (CGImageRef)createRasterCGImage:(CGImageRef)compressedImage { CGImageRef sourceImage = compressedImage; size_t width = CGImageGetWidth(sourceImage); size_t height = CGImageGetHeight(sourceImage); size_t bitsPerComponent = 8; size_t bytesPerRow = 4 * width; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedFirst); CGContextDrawImage(context, CGRectMake(0, 0, width, height), sourceImage); CGImageRef rasterImage = CGBitmapContextCreateImage(context); CGColorSpaceRelease(colorSpace); CGContextRelease(context); return rasterImage; } |
これでオッケー!
と言いたいところですが、このコードはまだ最適化の余地があります。
処理を追いかけてみると、
1、CGBitmapContextCreateでビットマップコンテキストを生成
この箇所でまずラスターデータがwidth x height x 4 バイト分裏で確保されています。
2、CGContextDrawImageでラスター展開を行いながら、ビットマップコンテキストに画像データを移動
3、CGBitmapContextCreateImage()により、コンテキストからCGImageRefを生成します。
という処理になります。
なので、1で一度ラスターデータを確保して、3で再び確保してコピーしているのです。
これは非常に無駄です。
なので、以上をふまえて、さらに最適化したコードは以下の様になります。
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 |
+ (CGImageRef)createRasterCGImage:(CGImageRef)compressedImage { size_t width = CGImageGetWidth(compressedImage); size_t height = CGImageGetHeight(compressedImage); size_t bitsPerComponent = 8; size_t bytesPerRow = 4 * width; size_t bytesSize = bytesPerRow * height; uint8_t *bytes = malloc(bytesSize); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(bytes, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedFirst); CGContextSetBlendMode(context, kCGBlendModeCopy); CGContextSetInterpolationQuality(context, kCGInterpolationNone); CGContextDrawImage(context, CGRectMake(0, 0, width, height), compressedImage); CGContextRelease(context); CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, bytes, bytesSize, bufferFree); size_t bitsPerPixel = 32; CGImageRef rasterImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedFirst, dataProvider, NULL, NO, kCGRenderingIntentDefault); CGDataProviderRelease(dataProvider); CGColorSpaceRelease(colorSpace); return rasterImage; } |
このコードでは
1、ラスター用のメモリを確保する
2、ラスター用のメモリを使ったビットマップコンテキストを作成する
この場合、裏でメモリは確保されず、こちら側で用意したメモリが使われます。
3、CGContextDrawImageでラスター展開を行いながら、ラスター用のメモリに画像データを移動
4、確保していたラスターデータから直接CGImageRefを生成する
という流れになり、
ラスターデータのコピーを避けることができていることが分かると思います。
非常に優れたiOS SDKですが、
このようなちょっとしたことで大きくパフォーマンスが変わることは往々にしてあるので、
注意深く見ていきたいですね。
特に高レベルなAPIを使うとなかなか気づきにくいものです。