2010年5月30日日曜日

Core Data のパフォーマンスをちょっとだけ調べてみた

ちょっと仕事で触ってみて分かった範囲のことを書きます。断りがない限り、 iPhone 3GS で Wifi 接続環境下においてテストしました。

■キャッシュ無し vs キャッシュ有り

executeFetchRequest:error: メソッドを用いて、 Entityのプロパティで一件だけ絞り込んで返すようなクエリは大変遅いということが分かりました。Indexを付けて実行してもほとんど速くなりません。どうやらそもそもバックエンドに使っているSqliteが大変遅い、特にコネクションを生成したり破棄したりするのが遅い感じがするので、ループで一件ずつ取得するなどのときはたくさんのSQLが実行されないようにする必要があります。 objectWithID: メソッドは試していないのでちょっと不明です。

回避策として、アプリが起動したタイミングで当該エンティティの全オブジェクトをあらかじめ取ってきて、 NSMutableDictionary にでも突っ込んでおく。次回以降のフェッチはその NSMutableDictionary から行う、と言うようにすると凄く速くなりました。

実測値は以下の通り。
pre loading time というのが自前のNSDictionaryキャッシュの事前生成、parseResponseBodyというのがXMLの解析で、この中に大量のCore DataオブジェクトをDBから引っ張ってくる処理が含まれています。
//////////////////
プリキャッシュなし
//////////////////

2010-05-25 12:11:37.254  FetchAllModelA parseResponseBody time : -1.214856
2010-05-25 12:11:39.674  FetchAllModelB parseResponseBody time : -2.416063
2010-05-25 12:11:41.097  FetchAllModelC parseResponseBody time : -1.384185

//////////////////
プリキャッシュあり
//////////////////

2010-05-25 14:12:14.788  pre loading time : -0.039540
2010-05-25 14:12:17.518  FetchAllModelA parseResponseBody completed. time : -0.836754
2010-05-25 14:12:20.719  FetchAllModelB parseResponseBody completed. time : -1.312910
2010-05-25 14:12:19.169  FetchAllModelC parseResponseBody completed. time : -0.902832
ほんの0.03秒のプリキャッシュ処理のおかげで、XML解析が最大で1秒以上短縮できています。とにかくCore DataがSQLを飛ばさないように調整すると効果があるみたいです。


■DB書き込み速度

NSManagedObject の生成はメモリ上 (NSManagedObjectContext) で行われるためなかなか高速なのですが、それを save するのがとにかく iPhone 3G で顕著に遅く、 300件程度のデータを保存するのに5秒以上かかってアプリが正常終了できないという事態が発生しました。リレーション張りすぎたかなーと思います>< iPhone 3GS なら2秒3秒程度で間違いなく完了するので特に問題になっていません。対策としては暇なときに逐一DBにsaveするか、どうでもいいデータはiPhone 3Gでは保存しないとか、一時エンティティにするとか。

iPhone の NSURLRequest で gzip 使ってみた

http://stackoverflow.com/questions/2682483/nsurlconnection-nsurlrequest-gzip-support

こちらに記載があるとおり、 Cocoa の NSURLConnection はgzip圧縮されたコンテンツを自動的に解凍して扱ってくれるみたいです。gzipでリクエストを受け取るには、以下のように NSURLRequest のヘッダに値を追加します。
[urlReq setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
これだけで後はすべて自動的にやってくれます。もちろんサーバー側がきちんとgzip圧縮して返すように設定されていなければなりません。


■パフォーマンスを試してみる

サーバーと通信を行い、レスポンスとして1000行程度のXMLを取得して解析するアプリで実験してみました。iPhone 3GS, Wifi回線を使用。

圧縮無しの時の結果はこちら。
API①
16:11:58.822 開始
16:12:04.126 レスポンス受信
16:12:04.184 データ受信
16:12:05.322 パース完了

API②
16:11:58.838 開始
16:12:04.118 レスポンス受信
16:12:04.433 データ受信
16:12:07.276 パース完了

API③
16:11:58.847 開始
16:12:04.718 レスポンス受信
16:12:05.822 データ受信
16:12:08.299 パース完了
圧縮ありの時の結果はこちら。
API①
16:03:43.056 開始
16:03:46.349 レスポンス受信
16:03:46.414 データ受信
16:03:48.473 パース完了

API②
16:03:43.080 開始
16:03:46.326 レスポンス受信
16:03:46.671 データ受信
16:03:49.905 パース完了

API③
16:03:43.088 開始
16:03:46.308 レスポンス受信
16:03:46.371 データ受信
16:03:47.532 パース完了
一回しか試していないのでムラはありそうですが、通信自体は確実に早くなっている気がします。3Gで計測すればさらに顕著な違いになると思うのでおすすめです。

iPad 3G を Xcode につないだら対応していない OS と言われてしまった件



昨日の勉強会でさっそく買ったばかりのiPad 3GをXcodeにつないでみたら、ご覧のように対応していないOSであるとエラーが出てしまいました。

再度iPhone Developer CenterからSDKをダウンロードしてインストールし直したら問題なく動作するようになりました。おそらく単にSDKが古かっただけなのだとは思いますが、一応。

第 2 回西東京 iPhone 勉強会に行ってきました

http://atnd.org/events/4014

メンツがそうそうたるメンバーでびっくりしました。内容も非常に充実していて、 iPhone 技術ネタとしては Bonjour と UIGestureRecognizer 、iPhone 技術以外のネタとしては iPhone アプリのネタだしの仕方と iPhone アプリで受託案件をした際のお話、最後に HTML5 で Keynote みたいなのを作ったよと言う話でした。

技術ネタは予習しておいたおかげで、聞くだけになってしまわなくてすごくよかったです。 UIGestureRecognizer は UIView.multiTouchEnabled を無視して複数指のジェスチャが取れるというネタが大収穫。あとは UIWebView のタッチを無理矢理検出する方法とか。

個人的には iPhone アプリで受託案件をした際の経験談が非常に面白かったです。作る方はまだ分かるのですが、契約だとかお金の話だとかそういう話になってくるとなかなか参考になるものがないので、非常に貴重でした。次回もこのネタは聞きたいです。

HTML5 は可能性を見た感じです。マルチタッチも検出できているし簡単なアニメーションも CSS で実装できているみたいで、 JavaScript (笑)なんて言えなくなりそう。

予想通りというか iPad 率は6割超えてました。 iPhone アプリリリース経験者も5割以上。

最後に懇親会で iPadによる iよせがき を試してみました。はい、単に岸川さん高山さんにiPad上にサインを貰っただけです>< ありがとうございます!

2010年5月24日月曜日

Core Data では モデルの delete - insert をしない方が良い

最近お仕事で Core Data を頻繁に使っているのですが、ちょっとだけハマったケースをご紹介します。 Core Data ではモデルの delete - insert による更新をしない方が良いようです。


■delete - insert が問題になるケース

たとえば、 Core Data から全く同じ内容をフェッチして表示する二つのUITableViewがあるとします。それぞれ A および B と名付けます。
ここで、 A を表示してから、 B を表示する際に Core Data モデルを delete - insert すると、 A を再度表示した際にアプリケーションがクラッシュします。原因はおそらく、 A で既に読み込まれているモデルを B が削除してしまっているため、 A で既に削除されているモデルをテーブルが表示しようとしてクラッシュしているものだと思われます。 A の viewWillAppear などのタイミングで、再度 Core Data からオブジェクトを取得し直せばこの問題は解決します。

そもそも NSFetchedResultsController を使えばたぶんこの問題は発生しないと思うのですが、いずれにせよ変更があったデータをフェッチしなおさなければならなくなってしまいます。ということで可能な限りプライマリキー項目を設定して update で対応することをお勧めします。ただし update は update で sqlite3 の実行がやたら遅いということが分かっているのでこちらはこちらで別途チューニングが必要です。詳細はまた別エントリーにて。

2010年5月4日火曜日

Byline 3 ヘルプ と Tips

Byline 3 のちょっとした日本語ヘルプとTipsをこちらに掲載しようと思います。

※2010/05/11よりBylineの日本公式アカウント @bylinejp を開設しましたので、是非ご活用ください。


■メインフォルダって何?

設定画面の同期セクションにあるメインフォルダという設定。これは、簡単に言えば Bylineで表示するフォルダ です。デフォルトでは「すべてのアイテム」になっています。この状態だと、 Google Reader 側にあるすべてのフォルダがBylineと同期されて画面に表示されます。メインフォルダを設定することで、 Google Reader 側の指定されたフォルダのみをBylineと同期して表示する事ができます。一部のフィードのみを同期したい場合に有効です。

注意点として、 一度に指定できるメインフォルダは一つだけ です。


■Webページのキャッシュ設定について

設定画面のキャッシュセクションにあるWebページという設定。こちらの内容が少々難解になっているので解説したいと思います。


  • モバイル向けデザイン
    この設定をONにすると、外部Webサービスを使用して対象のWebページのコンテンツを「モバイル向けデザイン」に変換してキャッシュします。モバイル向けデザインにすると、CSSが除去されたり、イメージが除去されたりリサイズされたりします。また、全体的に縦長なデザインになり、モバイルデバイスで読みやすい形式になります。
  • 自動でキャッシュするコンテンツ
    メインフォルダ、スター付きアイテム、メモのうち、同期時に自動的にキャッシュするものを選択します。画像はメインフォルダが「すべてのアイテム」にセットされている場合の物ですが、メインフォルダを変更するとここの名前も変化します。この設定項目をONにした場合、以下のようなポリシーでキャッシュが行われます。
    • メインフォルダに含まれるフィードは、次の「フィードごとのキャッシュ設定」で設定した内容に従ってキャッシュされます。
    • スター付きアイテムに含まれるフィードは、すべてのフィードがキャッシュされます。
    • メモに含まれるフィードは、すべてのフィードがキャッシュされます。


  • フィードごとのキャッシュ設定
    メインフォルダに含まれるフィードのキャッシュポリシーを設定します。 この設定が適用されるのはメインフォルダに含まれるフィードのみです。 スター付きアイテムおよびメモには適用されず、常にすべてのフィードがキャッシュされます。
    一覧にある各フィード名をタップすると、以下の三つの状態が順番に変化します。
    • 無印(デフォルト状態)
      本文が意図的に削られていると判断されたアイテムのみを自動的にキャッシュします。すべての本文が含まれているアイテムはキャッシュしません。
      本文が意図的に削られているアイテムというのは、たとえば このような ニュース本文全体を乗せない RSS フィードの記事のことです。
    • √マーク付き
      本文が削られているか否かに関わらず、常にキャッシュします。
    • ×マーク付き
      本文が削られているか否かに関わらず、常にキャッシュしません。
    また、右側の図にある薄い色の√マークがついているフィードは、「Bylineによって本文が意図的に削られていると判断されたフィード」であることを示します。この薄い色の√マークはメインフォルダを自動でキャッシュする設定にしているときにのみ表示されます。左側の図のCNET Japanにはチェックが無くて右側の図のCNET Japanにはチェックがあるのはこれが原因です。


■Twitter連携の設定方法

BylineではOAuthを使用してTwitterアカウントにログインします。残念ながらこのOAuthの画面が英語のままになっているため、こちらで代わりまして簡単に解説いたします。



設定画面のアカウントセクションにあるTwitter欄をタップすると、上のような画面になります。ユーザー名とパスワードを入力して、ログインボタンを押してください。



この画面が正常に表示されれば完了です。画面右上の「完了」をタップして終了してください。なんか下の方で Redirecting you back to Byline とか表示されちゃってますが無視してOKです。


■Twitter投稿時にURLを短縮する方法

Twitter投稿画面には一見、URLを短縮するためのボタンがありません。ですがご安心ください。BylineにはURLを自動的に短縮する機能があります。



このように普通に本文をタイプして、140文字よりも長くなってしまうと・・・



ご覧の通り!自動的にURLが短縮されます。



さらに本文が長くなってそれでも投稿できなくなってしまったらこのようになります。どうでしょうか、いちいちボタンを押して縮めるよりスマートだと思います。


■裏技:Byline 2と同じ感覚で「すべて既読にする」方法

Byline 3ではUIが変更されて、Byline 2ではアイテム一覧の一番下にあった「すべて既読にする」ボタンがありません。個人的にはこの変更はすごい抵抗があって作者の人にも何とかならないかとお願いしてみたのですが、その代わりにちょっとした裏技を付けてくれました。



画面一番下のツールバーをスワイプすると、ツールバーに「すべて既読にする」ボタンが表示されて、すべて既読にすることができます。これで多少はByline 2に近い感覚で操作できるかと思います。

2010年5月3日月曜日

Byline 3 の新機能まとめ



ちょっとした宣伝ポストになりますが、私が日本語翻訳を手がけてます iPhone の RSS リーダーアプリ Byline のバージョン3が近日リリースされます。すでにリリース候補ビルドが完成しており、特に何の問題もなければ来週月曜日 (5/10) にリリースされる予定です。既存の Byline 2 ユーザーの方はそのまま無料でアップデートすることが出来ます。

バージョン2にくらべて高速で動作し、文字エンコードの取り扱い方法を変更したため以前よりクラッシュしない・・・はずです。少なくとも最新バージョンになってからは私のクライアントでは一度もクラッシュしておらず、安定性は上がっていると思います。

Byline 2 からの変更点とか新機能とかを以下にまとめてみました。


■Google Reader との同期速度向上

Google Reader と同期する際のスピードがさらに高速化しました。特にすべてのフィードのインデックスを生成する速度が劇的に改善されています。具体的にどれぐらい早くなったのか、私の iPhone 3GS 上で実際に測定してみました。

Byline 3


Byline 2


使用したバージョンアイテム件数同期にかかった時間アイテム/秒
Byline 2.5.6511件340.43秒1.50
Byline 3.0f1275件135.99秒2.02

ごらんのように、およそ3割程度速度が向上していることがわかります。アイテム一件一件のキャッシュ速度はそれほど変わりませんが、最初のインデックス取得が劇的に高速化している分だけ全体にかかる時間が短縮されているようです。


■アイテムのキャッシュ機能強化

目玉であるWebページのキャッシュ機能も強化されています。今回のバージョンから、 途中で省略されているアイテムのみをキャッシュする ことが可能になりました。途中で省略されているアイテムというのは、たとえば このような ニュース本文全体を乗せない RSS フィードの記事のことです。 Byline はこのような省略されている記事のみを判定して自動的にキャッシュしてくれます。


■インターフェース変更

はい、 Byline のバージョンアップといえば毎度おなじみのUI変更とアイコンの変更です>< 2までの操作に慣れた方(自分含む)には申し訳ありませんが、また慣れてください・・・その代わり前のインターフェースより優れているところも結構あります。



メイン画面はこんな感じです。フォルダの中の個別のフィードごとに記事を読めるようになりました。



記事一覧。リストの一番下にあった「すべて既読にする」がなくなり、その代わり右上に「編集」ボタンがついてそちらから一度に既読に出来るようになっています。



記事詳細も変わっています。画面左下にあったキャッシュを見るボタンが右上に移動しました。



こんな具合に記事詳細を左右にスワイプすることでつぎつぎと記事をめくっていくことが出来ます。

■Twitter / Instapaper / Read it Later との連携



最近の RSS リーダーではおなじみの外部連携が強化されています。 Twitter / Instapaper / Read it Later と連携することが出来ます。写真は Twitter への投稿画面。結構良くできています。リンクが長すぎる場合には自動的に短くしてくれたりします。


■まとめ

最近は Byline よりも多機能でより多くの外部サービスと連携できる RSS リーダーが登場してきていますが、今回のアップデートで外部連携が強化されたり個別のフィードを読めるようになったりと、それらの最新アプリと対抗できるだけの機能がついてきたかなと思います。目玉の Web ページキャッシュ機能もさらに強化されています。あとは安定動作してくれればまず負けないと思うのですが・・・こればっかりはリリースして皆さんに使っていただかないとわかりません >< とはいえ私の環境では500件以上のフィードを一度に読み込んでも安定しており、前バージョンよりさらに高速化しています。ご期待ください!

UTF-8 の文字列の長さを正確に求めるためには Normalize しましょう

Twitterにメッセージを送信する際に、クライアント側でメッセージの長さを判定して画面に表示したいというような要件があると思います。当然、このような場合には入力されたメッセージを取ってきてその長さを求めてやれば良いのですが、どうやら UTF-8 などのマルチバイトな文字コードを使っていると文字列の長さを正確に求めるのが大変なようです。


■何がどう大変なのか

図を書いてみました。「文」が文字数、「b」がバイト数を表しています。 Objective-C の場合は、「文」が ``[NSString length]`` を、「b」が [NSString lengthOfBytesUsingEncoding:NSUTF8StringEncoding] を表します。ほかの言語の場合もそれぞれ相当するメソッドまたは関数が用意されていると思います。



上のケースが一般的な UTF-8 文字列です。この場合、バイト数はマルチバイトになってしまうため実際の文字数(我々人間が見て自然な文字数)とは一致しませんが、その代わり UTF-8 文字数が実際の文字数と一致するため問題なく長さを求めることができます。

面倒なのが下のケース。ウムラウト記号や濁点・半濁点が一文字として入力されてしまっている場合です。通常滅多にこのような文字列に遭遇することはないのですが、まれにこのような文字列を入力させる処理系があるようです。で、この場合はウムラウト記号や濁点・半濁点も一文字として認識されてしまうため、実際の文字数と文字列長判定メソッドが返す文字数が異なってきます。そこまで厳密にしなくてもよいのでは・・・と思ったのですが、なんと Twitter はきちんとこのような場合も自然な文字列の長さを測定して140文字かどうかを判断しているらしいです。


■対策:Normalizeする

このように実際の文字数と UTF-8 上の文字数が異なっている場合には、 Normalize と呼ばれる処理を行って文字を圧縮する必要があります。 Objective-C の場合には以下のようなメソッドが標準で用意されています。
// NFD
– (NSString *)decomposedStringWithCanonicalMapping

// NFKD
– (NSString *)decomposedStringWithCompatibilityMapping

// NFC
– (NSString *)precomposedStringWithCanonicalMapping

// NFKC
– (NSString *)precomposedStringWithCompatibilityMapping
これらのメソッドは、 Unicode 文字列をそれぞれ NFD, NFC, NFKD, NFKC と呼ばれるフォーマットの文字列に変換してくれます。それぞれの文字列がどのような表現を表しているかは、以下のページを参考にしてみてください。英語ですが図説がついてるので非常にわかりやすいです。
http://unicode.org/reports/tr15/
http://homepage1.nifty.com/nomenclator/unicode/normalization.htm

ほかの言語、たとえばPythonやRubyなどでこの機能が提供されているかどうかはわかりません。あしからず・・・><