Core Data で画像を扱う

前回の記事 で、「 Core Data によって、プログラムの骨格を作るのはかなり楽になるけれど、それだけでちゃんとしたプログラムができるわけではないし、もしそれができなければ、いま作っている Kaku の画像挿入機能は搭載しない」 ということを書いたと思います。

昨日はまさに、そういう「これじゃあ公開できない」という事態に直面していました。前回の記事を書く前に、うすうす気付いてはいたのですが、やはり大量の画像を登録したとき、画像挿入機能のパフォーマンスがかなり悪くなる のです。

結局原因は、Core Data に頼りすぎた、とかではなく、設計そのものがおかしかったというか、単純に、もっと勉強してから臨むべきだった、ということでしたが…(汗)

…というわけで今日は、その問題を解決していった過程を書いていきたいと思います。タイトルは「Core Data で画像を扱う」となっていますが、それについての 正しい答えが書いてあるわけではありません ので、悪しからず(僕が今のところどういった方法でやることにしたかは、書いてあります)。

何しろ、プログラム開発の専門教育を受けたこともないし、もろ文系なので、あまり笑わずに(?)読んでください。何かもっといいアドバイスがありましたら、ありがたく頂戴いたします。

Core Data で画像を扱う

僕が画像を扱わなくてはならない理由は、画像挿入機能で Kaku に登録された画像のサムネイル です。

本当に初期のテストでは、Core Data で画像ファイルのパスだけを保存しておいて、NSImageView の「 valuePath 」バインディングで表示させていましたし、オリジナルの画像データそのものを Core Data で保存してしまう、という方法も考えられますが、それだとどうしても パフォーマンスが悪くなる ので(…と言っても、最初の方法での動作がどんな感じだったかは、ちょっと忘れてしまった)、それならば、サムネイルを作って、一緒に保存しておこう、ということになります。

そこで、そのために僕がとった方法を順に追っていきますが、僕が最初にとった方法は:

  • Cocoa で画像データを読み込むのって、どうやるんだっけ? [[NSImage alloc] initWithContentsOfFile:path] とかでいい?

  • Core Data では、直接 NSImage を扱うことはできないけど「バイナリ」なら扱える。

  • じゃあさっきの NSImage のインスタンスをアーカイブ化して保存しよう。

…という、あまり深く考えていないものでした。

方法1

  1. エンティティに、種類 = バイナリ の属性を追加する。

  2. このエンティティに基づいた管理対象オブジェクトを生成するとき、上の属性に、以下のようにして生成したサムネイルデータを設定する。

    • 画像ファイルから NSImage のインスタンスを生成。

      NSImage *thumbnail = [[NSImage alloc] initWithContentsOfFile:originalFilePath];

    • 縮小……?

      [thumbnail setScalesWhenResized:YES]; [thumbnail setSize:thumbnailSize];

    • アーカイブ化して属性値として設定。

      [managedImage setValue:[NSKeyedArchiver archivedDataWithRootObject:thumbnail] forKey:@"thumbnail"];

  3. 「 PPMKeyedUnarchiveFromData 」といった Value Transformer を作っておく。(ここではプリセットの「 NSUnarchiveFromData 」は使えないし、そもそも NSArchiver/Unarchiver は非推奨)

  4. 表示するときは、NSImageView などの「 value 」バインディング にバインドし、3. の Value Transformer をセットする。

方法1 — 結果

僕はこのサムネイルを NSTableView で表示するように設定したのですが… ハードディスクがすっごいガリガリ言ってる! スクロール重い!

たぶんメモリ使用量はそんなに多くないと思いますが、どうもテーブルの新しい行が表示されるたびにアーカイブ化/非アーカイブ化が連発するようなので、ぜんぜん快適に表示できません。これは失敗でした。

方法2

これに懲りて、もうちょっとちゃんと調べてみることにしました。

調べてみると、画像のように そのままでは Core Data で扱えないデータを扱う方法 を紹介したページとして、CocoaDev の「 BinaryInCoreData 」というページを発見しました。あとから調べてみると、アップルのドキュメントにも、これに関連した ページ が。

方法2 はこれらのページを参考にしたもので、詳細はリンク先を読んでいただくとして、簡単に書くと:

  1. エンティティに以下の属性を追加:

    • 実行時用の 種類 = 未定義一時 = YES の属性

    • 保存用の 種類 = バイナリ の属性

  2. NSManagedObject のサブクラス を作り、それを上記エンティティのクラスとする。

  3. このサブクラスで、実行時用属性のアクセッサメソッド(など)をオーバーライドする。- (id)<実行時用属性名>- (void)set<実行時用属性名>:(id)newValue など )

    オーバーライドしたメソッドで何をさせるかというと:

    • 取得メソッドが呼ばれたら、実行時用の属性がすでに読み込まれているかをチェック( [self primitiveValueForKey:@"<実行時用属性名>"])して、まだ読み込まれていなければ、保存用の属性(バイナリデータ)を読み込んで、それを非アーカイブ化して、実行時用の属性として設定する。

    • 設定メソッドが呼ばれたら、それを実際に設定するほか、(必要なら)それをアーカイブ化して、保存用の属性に設定する。

    • (さっきから「など」を連発していますが)例えば - (void)awakeFromFetch をオーバーライドすれば、実行時用属性を読み込むタイミングを早めることができる(「まさにその実行時用属性を取得しようとしたとき」→「(ほぼ)プログラムが起動したとき」)。

僕の説明ではよく分からないかもしれませんが、要は 方法1 とは異なり、ハードディスクからデータを読み込んで、非アーカイブ化するのが(ほぼ)1回 になります。ガリガリしません。

「これは賢い!」 と思って早速この方法でやってみました。ところが…

方法2 — 結果

  • 最初は パフォーマンス良好。
  • でも画像を 10 コ強登録していくと、だんだん動作が重くなってくる。
  • よく考えなくても分かることだけど、アクティビティモニタ.app で見たら メモリ使用量がスーパーインフレ。( 通常 20MB弱 → 100MB強
  • Kaku.xml ファイル(永続ストア)の容量は、だいたい 300KB の画像を登録すると 3MB 増えるペース(!!)

方法1 も 方法2 も、扱うデータの種類が違えば、適用できる場面もあるに違いありません。しかし、画像データ、かつ数が不定、というこの場面では、ちょっとキツいものがあります。

このあと、「じゃあサムネイルデータがいらなくなったら解放しよう」 と考えたのですが、これが罠でした。サムネイルがテーブルの枠外に出たときがベストタイミングですが、これを判定するのはかなり難しそう。じゃあメモリ上に読み込んでおけるサムネイルの数を制限する? これもちょっと難しそう。画面上で見えなくなったテーブルの行が判定できたとして、そもそもどうやって、その行のサムネイルデータを持っている管理対象オブジェクトを探し出すのか…。後から考えるとかなりアホらしいのですが、このときは、もはや開発続行不可能か と思われました。

…ここまでが、前回の記事 を書いた後までの話。

方法3

結局どうやったら解決できるのか分からず、とりあえず Cocoa での画像の扱いや、どうやったら画像を早く処理できるのかを勉強し直してみることにしました。その中で、「 aaron evans » Blog Archive » CoreImage vs NSImage scaling 」という記事に行き当たったのですが、この記事を読んで、(本当に、かなり遅ればせながら)2つのことに気付きました。

  • これまでなんとなく NSImage をディスクに保存したり、読み出したりしていたけど、NSImage は高度な Cocoa オブジェクトであって、画像そのものを扱いたいならかなり無駄だし、いろいろコストがかかりすぎる。

  • (これは上ほど重要ではありませんが)てか、「方法1」の サムネイル生成の手法も、ちょっと適当過ぎかもしれない。(上の記事に書いてある方法が、画像の拡大縮小に関しての「 The classic way(=鉄板)」らしい)

これを踏まえて作戦を考えると、

  1. エンティティに、種類 = バイナリ の属性を追加する。

  2. このエンティティに基づいた管理対象オブジェクトを生成するとき、上の属性に、前述の記事を参考にして元画像を縮小、かつ、ダメ押しで JPEG 圧縮して生成したサムネイル画像の バイナリデータ( NSImage のインスタンスではない )を設定する。

  3. 表示するときは、単に NSImageView などの「 data 」バインディングにバインドする。

方法3 — 結果

  • 激速。あまりに速いので、楽しくてしばらくスクロールし続けて遊ぶ。
  • 試しに 画像を80コほど登録 してみたが、スピードは変わらず。
  • メモリ使用量は普段と変わらず。
  • Kaku.xml ファイルの容量は、ほぼ 画像数 × 10 = 800KB くらい。

…というわけで、「『方法3』が見事!!」と言うより、僕の当初の発想が、見事なまでに間違っていた というべきですが、これで、画像挿入機能の搭載自体を取りやめる、という事態は避けられました。「 Core Data を使わなければいいのかな…」とも考えましたが、これでまたしばらくは、爽快な Core Data を使い続けていけそうです。

本当は、前回のようにもう1セクション設けようと思っていたのですが、長くなりすぎたので(読んでくれる人いるのか?)、自粛。

※業務連絡:フィードを全文配信にしてみたつもりですが、成功しているか分かりません。この記事でチェック。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です