iPhone版LINEのデータ構造 : Core data in LINE

こんにちは。検索サービス開発4チームでメッセージアプリのLINEのiPhoneアプリ開発を
担当している金泰敬(キム テギョン)です。

今回説明させて頂きたい主題はLINEのモデル側を支えているCore Dataです。
Core Dataは、MacOS XのベースFrameworkであるCocoaのMVC構造のうち、
Model側を担当しているFrameworkです。

Core Dataを利用するとデータモデルの設計、オブジェクトのデータの読み取り、書き込み、管理などを簡単に行うことができます。
現在、LINEではCore Dataを利用してメッセージ、トーク、グループ、ユーザーなどを管理しています。

例えば、相手のメッセージが到着するとまずコアのデータからSqliteDBに格納します。
そして保存されたメッセージに関連しているトークでも更新が行われます。
もしそのメッセージが現在見ているトークと関連がある場合は画面上で自動的にメッセージが更新されます。

これらのすべての作業をする為に長いコードは必要ありません。
iPhoneで再設計されたCore Dataのクラスを利用すれば簡単に実装することができます。

中心になるクラスはNSFetchedResultsControllerです。

このクラスの役割はデータの更新を監視して今見ている管理対象のデータであれば
変更内容についての処理を委任(delegation)クラスに通知(NSNotification)します。

NSFetchedResultsControllerDelegate
- controllerWillChangeContent:
- controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
- controllerDidChangeContent:

上記のクラスが提供する3つのprotocolを実装すると簡単にデータの通知を扱うことができます。

........
    //検索条件を作成
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Message" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    [fetchRequest setFetchOffset:fetchOffset];

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chat = %@", chatObject];
[fetchRequest setPredicate:predicate];

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timestamp" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];

[fetchRequest setSortDescriptors:sortDescriptors];

// FetchedResultsController 生成
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
        managedObjectContext:self.managedObjectContext
//データの更新通知を自分に委任
fetchedResultsController.delegate = self;

........

クラス内部のProtocolを実装

- controllerWillChangeContent :
メッセージビューに更新を準備させる

  • controller : didChangeObject : atIndexPath : forChangeType : newIndexPath :

データの更新を適用する

  • controllerDidChangeContent :
    メッセージビューに更新を表示する。

上記のように検索条件を設定し、データの更新を処理するメソッドだけを
実装すると簡単にデータの更新をリアルタイムで表示することが出来ます。

で、どうやってこんな機能が実現されているのか?
このCore Dataの技術を理解するには知っておかなければならない概念が幾つかあります。

1. NSObject
基本的にObjective-Cの殆どのクラスはNSObjectを継承しています。

NSObjectはプログラミングに多様で便利な機能を提供するルーツクラスです。
Objective-CでObjectとして機能するためにはNSObjectクラスを必ず継承する必要があります。

2. Dynamic binding(Dynamic dispatch)
Objective-Cは、Dynamic bindingをサポートしています。

id rtn = [obj message];

objのmessageは、実行時に決定されます。

コンパイルするの時にmehtodのタイプのマッチングを制限しないため、柔軟なプログラミングが可能です。

3. Selector
dynamic bindingによって可能になる特徴の一つでselector変数があります。

以下のように

SEL method = @ selector(methodName)

の形で呼び出されるmethodを作成してParameterとしてObjectを渡すことができます。

例えば、

[target description];

SEL descriotion = @ selector(description);
[target performSelector:descriotion];

同じ意味です。

この非常に単純に見える技術によってdelegate patternを実装することがとても簡単になります。

※内部的にすべてのmethod名はSEL型としてimmutable objectとして保存されます。
つまり同じ名前のmethodは同じ変数を指しています。

4. Target Action Paradigm
SEL変数があるため可能になったパターンです。

UIButton * button = [[UIButton alloc] init];

[button addTarget:self action:@ selector(doSomething:)forControlEvents:UIControlEventTouchUpInside];

ボタンを作成して、上記のように実行するmethodとターゲットobjectを一緒に指定して
実行を委任することができます。

5. KVC(key - value coding)、KVO(key - value observing)
個人的にはObjective-Cの中で最も面白い技術の2つです。

KVCによってObjectのPropertyのデータを変数名として読み書きすることが可能になります。
これもSelectorで可能な技術としてsetName、name(getName)を自動的に作成して
呼び出すことが出来ます。

KVCの種類
valueForKey - キーとして値を探す
setValue forKey - 値をキーとして設定する
valueForKeyPath - の値をキーのパスとして探す(department.member.name)

例えば、

- (id)tableView:(NSTableView *)tableview
      objectValueForTableColumn:(id)column
                            row:(int)row
{
    ChildObject * child = childrenArray objectAtIndex:row];
    if([column identifier] isEqualToString:@"name"]){
        return [child name];
    }
    if([column identifier] isEqualToString:@"age"){
        return [child age];
    }
    if([column identifier] isEqualToString:@"favoriteColor"]){
        //etc ...
    }
    //etc ...
}

上記の一般的なコードが

- (id)tableView:(NSTableView *)tableview
      objectValueForTableColumn:(id)column
                            row:(int)row
{
    ChildObject * child = childrenArray objectAtIndex:row];
    return [child valueForKey:column identifier];
}

KVCを利用すれば同じ動作が一行で可能になります。
ObjectがKVCに準拠することで他のObjectとの間の不必要なコードが少なくなります。

CoreData

KVOによってキーの名前とobjectがそのキーのpropertyの更新をObservingすることができます。
この実装はNSObjectでサポートしているのですべてのObjectはお互いにKVO、KVCを利用することがあります。

6. Support Tools - Object graph
Object Graphは、データモデルを設計するGUIツールです。
このツールを利用してData Objectをモデリングしデータ間の関係​​(relation)を設定することが出来ます。
GUIツールなので容易にデータの構造を把握し設計することが出来ます。

7. Core data
ついにCore Dataまできました。
NSManagedObjectContext
Core Dataを大きく分けると以下の3つの部分に分けることが出来ます。

NSManagedObject
- Core Dataの基本的なオブジェクトです。このクラスを利用してデータの読み書きが可能です。(KVC,KVO)
RDBに例えれば一つのROWに保存するクラスです。

NSManagedObjectContext

  • NSManagedObjectを管理します。またデータの更新を監視し関連するObjectに通知します。
    永続データ(persistent)の保存とロードを担当します。

NSPersistentStoreCoordinator

  • 永続データ領域を管理します。
    ストレージを内部の実装を気にせずに使える抽象化されたインターフェイスを提供します。
    (XML、SQLITEをサポートしますが、iPhoneではSqliteのみをサポートしています。)

では、一番最初に説明した機能の実装はどのように実現されているでしょうか?

相手のメッセージが到着するとまずコアのデータからSqliteDBに格納します。

  1. 保存されたメッセージに関連しているトークにも更新が行われます。
  • MessageとTalkの間に関係(relation)を設定してKVOを使用して変更を通知する。

2。もしそのメッセージが現在見ているトークと関連がある場合は画面上で自動的にメッセージが更新されます。

  • 監視を担当するObserver(NSManagedObjectContext)に変更の処理を委託する。(Target Action Paradigm)

それでは本日の説明は終わります。

長い文で足りない説明でしたが読んでいただいてありがとうございます。

.
.
.
.
.
.

1 more thing?

このまま終わったらちょっと物足りない人のために、
最後にiPhoneでCore Dataを使って経験した注意点を説明します。

Core Dataを使用する場合、いくつか注意事項があります。

  1. Saveをなるべくまとめてする
    Core Dataの中で最もコストのかかる作業は、保存の作業です。
    データ処理NSManagedObjectConextがほとんどのメモリで管理しているため非常に高速です。
    実際のデータの変更は、保存時に実際のデータベースで同期化されます。
    これはdisk I/Oを発生するため、多くの場合、アプリケーションが遅くなる原因となります。
  2. UIViewの更新をMain Threadで実行する
    アプリケーションの反応を良くするために、Core DataがMain Threadではなく、Threadで実行される場合があると思うのですがデータの更新がUIに反映すると同時に、もしMain Threadではない場合は、画面が停止したりクラッシュの原因となる場合があります。
    UIViewは、Thread Safeしないので、常にMain Threadで実行しなければなりません。
  3. NSManagedObjectContextはThread safeではない。
    NSPersistentStoreCoordinatorはThread Safeですが、NSManagedObjectContextは
    Thread Safeはないため、Multi Threadの際に使用する場合は注意をしなければなりません。
    しかし、Core dataは、Multi Thread上のデータの更新についてMerge機能を提供していますので下記のようにNotificationを登録して、マージ処理をメインThreadで処理しないといけません。
// 生成したContextに変更を他のcontextに通知する。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(lineMerge:) name:NSManagedObjectContextDidSaveNotification object:mainManagedObjectContext];

//通知をMain Threadで処理する。
  • (void)lineMerge:(NSNotification *)notification
    {
    ......

    dispatch_async(dispatch_get_main_queue(), ^{
    [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
    });
    ....
    }

Bye~

refrence

Apple Developer Library
http://developer.apple.com/library/ios/#documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol

http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/KeyValueCoding/Articles/Overview.html
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html#//apple_ref/doc/uid/10000177i

Core Data: Apple's API for Persisting Data on Mac OS X
http://www.amazon.co.jp/Core-Data-Apples-API-Persisting/dp/1934356328/ref=sr_1_6?ie=UTF8&qid=1320598030&sr=8-6