タグ: Core Data

  • 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セクション設けようと思っていたのですが、長くなりすぎたので(読んでくれる人いるのか?)、自粛。

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

  • Core Data は素敵 / AppleScript Studio プロジェクトと Core Data

    最近、MarsEdit 2 を使って、それについての 記事 を書いてみたり、ecto 3 Alpha を使って 記事 を書いてみたりしていたわけですが、自分は?というと、Kaku の画像挿入機能を試しに作ってみていました。

    なぜいま画像挿入機能を作っているのかというと、他に作ってみたい機能や、他に作ってみたいソフトもあるのですが、現実問題、いまとりあえず足りないのは、Kaku の画像・リンク挿入機能あたりだろう、と。また、すでに書いたように、Mac 向けの二大ブログエディタを実戦で使ってみた結果、「やっぱり画像やリンクの扱いをなんとかしないと、記事を書くのがなかなかスムースにならない」ということを痛感したからでもあります。

    こういった機能を作るには、画像の扱いやら、外部ファイルの扱いやら、ドラッグ&ドロップの扱いやら、自分がこれまで学んできたことがほとんど役に立たなそうな領域(=避けてきた領域)に挑むことになるので、かなりビビっていますが(下にも弱気発言)、今日からしばらくは、その中で気付いたことを記事にしていきます(たぶん)。


    Core Data は素敵

    脱線(2)」で、Core Data ベースの Kaku「CoreKaku」を途中まで作ってみた、ということを書きましたが、このとき初めて実践した Core Data が楽しく、こういった機能を作るときは Core Data を使おう、と心に決めていました(というか、Core Data 以外でのやり方を考えるのが面倒くさいというか…)。というわけで、この画像挿入機能は、全面的に Core Data を使って作られています。

    まず練習用に、こんなプロトタイプを作ることにしました:

    • ドラッグ&ドロップで画像を登録して、管理するアプリ
    • データベースには、画像本体は置かず、画像ファイルのパスとサムネイルを保存
    • 画像のパラメータから img タグを自動生成

    しかしこれは、GUI 上の操作とコピペ(おい)で、2秒ほどで完成(ウソですが)。これ以上は、記事のデータと連携できないと意味がないので、早々と Kaku との統合に作業を移しました。Core Data 恐るべし。

    ss_image_manager.jpg

    (※ Core Data はこうして、中心となる機能が動き出すのはかなり早いのですが、かなり開放的、というか、本当にまともなプログラムとして仕上げていくのには、時間と知識が必要 な気がします。例えば、保存ファイルはデフォルトの1個のままでいいのか、とか、もし大量の画像が登録されても、そのままでパフォーマンスが出るか、とか。動くには動くんですが、それ以外の部分を僕の知識でカバーできるか心配。今は快調に動いているんですが、それができなければこの機能は載せないかもしれません。けっこう弱気。)

    AppleScript Studio プロジェクトと Core Data

    ここで、AppleScript Studio ベースである Kaku 上で Core Data が動くように しなければなりません。

    昔の僕なら「そんなことできるの?」と思っていたでしょうが、AppleScript Studio アプリも Cocoa アプリの変種(?)だ、ということを念頭に置けば、できないはずがありません。たぶん、非 Core Data の Cocoa アプリを Core Data 化するのと変わらないのではないでしょうか。

    必要な作業は:

    • Core Data.framework をプロジェクトに追加する。

    • 本来 Xcode で「ファイル」「新規プロジェクト」「Core Data Application」 とすると、始めから Core Data.framework を含んだプロジェクトとともに、データモデルファイル(「 AppName_DataModel.xcdatamodel 」)と、あらかじめ基本的なコードが書き込まれた、アプリケーションデリゲートのヘッダ/実装ファイル(「 AppName_AppDelegate.h/m 」)が入っている。

      これらは知識があれば自分で用意することもできると思うが、(とりあえずは)これらがあった方が楽なので、なんとかする。僕の場合は、別に「Kaku」という名の Core Data アプリケーションプロジェクトを作成して、そこから拝借。

    • あとは Objective-C/Cocoa + Core Data の知識があれば、call method でなんでもできるよ!(※素敵なパラドックス。そんな知識があるなら、なぜ初めからそれで作らないのか、と。笑 僕は AppleScript のほうが慣れているから、としか言いようがありません…ってそれもやっぱりおかしいような。)

    大体こんな感じでしょうか。

    逆にほとんど Objective-C/Cocoa で書いた Core Data Application の一部を AppleScript(Studio)で動かすことも可能。前述の CoreKaku がこのアプローチ。…ってこれ、よく考えるとちょっと便利かもしれないなぁ。Objective-C/Cocoa の部分と AppleScript(Studio)の部分は、僕の印象よりもはるかにシームレスに共存できるんだな、と感じました。

    ちなみに、こういった方法を最初に知ったのは applescript-studio ML のこの 投稿 です。できたら僕もまとめて、「Notes」コーナーに投稿しようと思います。

    ※ この記事は、かなり初期段階の画像挿入機能を備えた Kaku で作成しました。

    t_ss_kaku_with_image_manager.jpg