iOSサンプルコードのTopSongsを読んだメモ

会社でサンプルコード勉強会というのをやっていて、今回僕の発表の番だったのでざっくりTopSongsを読んだメモ。

Cacheの所まで読みたかったけど時間なかったからまとめもなく途中まで。

まずはReadMe.txtを読む

This sample shows you how to import data from XML into Core Data.

このサンプルはXMLからCoreDataへデータをimportする方法ですよ

The focus is on performance, as this operation can be an expensive one. The XML parsing approach is borrowed from the sample "XMLPerformance", which compares parsing with libxml's C API and parsing with NSXMLParser. This sample uses a modified version of the LibXMLParser class, renamed as iTunesRSSImporter. This is the primary class to look at for how data is parsed from XML and imported into Core Data.

パフォーマンスの話をすると、XMLのparseにXMLPerformanceというサンプルのlibxmlAPIを利用した
LibXMLParserをiTunesRSSImporterにリネームして使ってますよ。
そのクラスでXMLからparseしたデータをCore Dataにimportしてますよ。

DISCUSSIONの中身
  • MULTITHREADING(マルチスレッドについて)

Parsing data is an expensive task, and importing data into Core Data can be as well. In order to provide a good user experience, this kind of work should be done in the background. This can be achieved using one of several APIs, but NSOperation is the most straightforward and is the route chosen for this sample. Multithreading always introduces complexity into an application. On the iPhone, you should be extremely careful to ensure that UIKit view objects are only accessed on the main thread. Even something as simple as [UIApplication sharedApplication].networkActivityIndicatorVisible = YES can cause problems if invoked on secondary threads. To be safe, use NSObject's performSelectorOnMainThread:withObject:waitUntilDone: to forward messages to the main thread.

データをparseするのは負荷がかかるし、Core Dataへのimportも負荷がかかるから
よいUXを提供するためにはbackgroundでやるべきですよ。
これを実現するための方法はいくつかあるけど今回はわかりやすいNSOperationを使いますよ。


マルチスレッドは複雑ですよ。
iPhoneではUIKit viewオブジェクトはmainthreadからのみアクセスされるべきですよ。
mainではないthreadで

 [UIApplication sharedApplication].networkActivityIndicatorVisible = YES

みたいなことやったら問題が起こる可能性があるから、

NSObjectのperformSelectorOnMainThread:withObject:waitUntilDone:

を使いましょうね。(main thread上でmethodを実行しましょうね)

  • MEMORY FOOTPRINT(メモリ使用量について)

Parsing and importing often result in the creation of large numbers of autoreleased objects. To control the memory footprint during these operations, you should create and drain additional autorelease pools at discrete intervals.

parseやimportはautoreleaseされるオブジェクトを多数作る原因になることがありますね。
メモリ使用量をコントロールするために、個別のautorelease poolを作ったりdrainすべきですよ。

  • CORE DATA RELATIONSHIPS(Core Dataのrelationについて)

Creating Core Data objects is fairly straightforward, and the Core Data Programming Guide has a very useful article "Efficiently Importing Data". Importing, however, becomes much more complex when relationships are involved. Part of the complexity arises from the need to retrieve objects based on some criteria without keeping all objects in memory. For example, in this application, there are Songs and Categories. Each Song has a to-one relationship with a Category; the inverse relationship is that each Category has multiple songs. The XML data presents songs with categories inline rather than as separate data. So, in the process of creating a Song object, it is necessary to associate that Song with a Category, based on the name of the Category. It's not known whether the Category already exists, so the first step is to perform a "fetch" in Core Data. If the fetch does not return an object, then a new Category is created. Finally, the relationship between the Song and the Category is established.

Core Dataオブジェクトはわかりやすいし、Core Data Programming Guideには "Efficiently Importing Data"という
とても役立つ記事があるのだけども、relationがあるようなimportはとても複雑になるものなのですよ。
複雑さの一部は、メモリ上に全てのオブジェクトを保持せずにいくつかの基準でオブジェクトを取得する必要性からですよ。


たとえば、今回はSongとCategoryがあって、それぞれのSongは一つのCategoryにひもづいている、逆に言うと、それぞれのCategoryは多数のSongを持っているわけです。
でもXMLではcategoryとsongをinlineで別々に表示しているのです。


なのでSongオブジェクトを作る手順では、Categoryの名前に基づいてSongオブジェクトをひもづけていきたいのだけど、
Categoryがすでに存在しているかわからないから、まずCore Dataを"fetch"するわけです。
fetchしてオブジェクトが返ってこないなら新しいCategoryを作ります。
で、そうするとSongとCategoryのひもづけができるというわけです。

The problem with this pattern is that fetches are, relatively speaking, expensive. The cost is primarily a result of the need to interact with the underlying database, which often requires I/O. Doing repeated fetches in rapid succession - such as during data import - can be very inefficient. One way of avoiding the need to perform a fetch is to keep the objects you will need in memory, or at least keep their unique identifiers in memory, with a lookup table. Then you simply go to the lookup table, retrieve the ID associated with the key you are using (the Category name in this case) and then get the object associated with the ID. This works well, expecially in cases where the size of the lookup table is both small and known in advance. However, it has the potential to create a different kind of problem - low memory due to overconsumption by the table.

このパターンの問題はDBとやり取りするためfetchのコストが高いということですよ。
importする間中fetchを繰り返していたら非効率なのです。


これを避ける方法の一つはmemory上にオブジェクトまたはDBのkeyとなるようなものを保持することですよ。
このkeyをもつようなlookup tableはとても小さいサイズだし効率よく動きますね。
でも、これはテーブルによって過剰にmemoryが使われてしまうという問題が起こってしまいますね。

The easiest way to avoid the memory problem for large or potentially large data sets is to use a simple caching mechanism. This builds upon the lookup table by setting a fixed size to the table. If an item is in the table (a cache "hit"), it is simply returned. If an item is not in the table (a cache "miss") then a fetch is performed to retrieve that item and it is place in the table. When the table becomes full, an item must be evicted in order for the current requested item to be cached. How you determine which item is evicted is known as the "replacement policy". There are many different algorithms for this purpose, the best known being "Least Recently Used" or LRU. That is the algorithm implemented in the cache for this example.

大きいデータのために起こるメモリの問題を避ける簡単な方法はcacheを使うことですよ。

PACKAGING LIST一覧
  • AppDelegate
    • Configures the Core Data persistence stack and starts the RSS importer.
      • Core Dataの設定とRSS importerの初期化
  • iTunesRSSImporter
    • Downloads, parses, and imports the iTunes top songs RSS feed into Core Data.
      • DL、parse、Core Dataへのimport
  • CategoryCache
    • Simple LRU (least recently used) cache for Category objects to minimize fetching while controlling memory footprint.
      • メモリ使用量をコントロールするCategoryオブジェクトのためのLRU cache
  • SongsViewController
    • Lists all songs in a table view. Also allows sorting and grouping via bottom segmented control.
      • すべてのsongをリスト化するtable view。下のsegmented controlでsortとgroupができる
  • SongDetailsController
    • Displays details of a single song.
      • 曲の詳細表示
  • Song
    • Managed object subclass for Song entity.
      • Song entityのためのNSManagedObjectのサブクラス
  • Category
    • Managed object subclass for Category entity.
      • Category entityのためのNSManagedObjectのサブクラス

CoreDataのコードを読むにあたり、まずはこの3つを覚える

  1. Managed Object Model:
    • NSManagedObjectModel
    • Core Dataのモデル(.xcdatamodeld)ファイル
    • データベーススキーマ(構造)
    • 各オブジェクト(Entity)の定義
    • GUIを使って定義する
  2. Managed Object Context:
    • NSManagedObjectContext
    • アプリケーション内の1つのオブジェクト空間(scratch pad)
    • managed object(NSManagedObjectのサブクラスのinstance)のcollectionを管理する
    • 最も重要なもので、オブジェクトを取得するとき、挿入するとき、削除するとき、などこれを主に使う
      • DBから既存のrecordをfetch、Managed Object Contextに登録、挿入や削除などの操作、storeにcommit
      • commitするまではmemory内で保持される
    • コードを書く時にいつもこのManaged Object Contextのメソッドを呼び出す
  3. Persistent Store Coordinator:
    • NSPersistentStoreCoordinator
    • データベースコネクションみたいなもの
    • 何というデータベースなのかを設定し、オブジェクトをストアする際に使われる
    • managed object contextはいつもPersistent Store Coordinatorを通してセーブする必要がある

上の3つを図に表すとこんな感じ。

  • Persistent Object Store(永続ストア)はテーブルとレコードを持つデータベースに似てる。(実際、store typeにSQLiteがある。でもDBである必要はない)
    • iOSでは通常1つ
「3つのクラスはこんなところで現れるよ」という話
  • (Xcode4.2)New ProjectでMaster-Detail Applicationを選び、「CoreDataを使う」的なチェックをすると以下のpragmaが作られ、そこに上記3つのgetterメソッドが作られる
#pragma mark - Core Data stack
- managedObjectContext
- managedObjectModel
- persistentStoreCoordinator

ではコードをざっくり見てみよう

AppDelegateでやっていること
  • CoreDataの3つの役割の初期化(上の図のような関係を作る)
AppDelegate applicationDidFinishLaunchingから確認

lastUpdateを確認して、RSSを取得した方が良ければ取得。

既にデータがある場合は、その全てのオブジェクトを消していくよりも、storeごと消した方が簡単なのでremoveする

        // remove the old store; easier than deleting every object
        // first, test for an existing store

82行目からgetterで呼び出される形でCoreDataの初期化が始まる

AppDelegate persistentStoreCoordinator:
  • initWithManagedObjectModelを使ってManagedObjectModelを読み込む
    • ここでは同時に「NSManagedObjectModel mergedModelFromBundles:nilでbundleから.xcdatamodelを読み込みManagedObjectModelを作成」という操作もやってる
  • その後にtype(SQLiteとか)とURL(file path)を指定してPersistentObjectStoreを指定
AppDelegate managedObjectContext:
  • managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinatorで、persistentStoreCoordinatorを指定
メインのviewであるSongsViewControllerへ
  • fetchしてdataの取得、successならtableViewのreload
SongsViewController viewDidLoad:
  • NSManagedObjectContextDidSaveNotificationをaddObserverすることでdataがsaveできたらtableViewをreloadするようにしている
  • そして、とりあえず始めのfetch
ここで、CoreDataにsaveしたときの動きに注意
  • saveNotificationはsubthreadでsaveした場合にはsubthreadから呼ばれるため、Viewがmainで実行されるようにmainThreadで実行し直すようにしている
// This method will be called on a secondary thread. Forward to the main thread for safe handling of UIKit objects.
- (void)importerDidSave:(NSNotification *)saveNotification {
    if ([NSThread isMainThread]) {
        [self.managedObjectContext mergeChangesFromContextDidSaveNotification:saveNotification];
        [songsViewController fetch];
    } else {
        [self performSelectorOnMainThread:@selector(importerDidSave:) withObject:saveNotification waitUntilDone:NO];
    }
}
SongsViewController fetch:
  • fetchedResultsControllerをgetして、そのmethodのperformFetchを投げてsuccessならtableViewをreloadだけ
    • 得られたデータはtableViewのreload時にfetchedResultsController sectionsで取得
SongsViewController fetchedResultsController:
  • fetchRequestを作っている
    • SQLを作るような感じ
  • SongのEntityをsetし、あとはSegmentIndexの値でCategoryのconditionを付けるか付けないかを決定してるだけ
ということで、話をAppDelegateに戻してiTunesからのデータ取得方法(NSOperation)
  • AppDelegateでの初期化の最後にself.operationQueue addOperation:importerを実行
    • operationQueueはgetterになっていて、NSOperationQueueのインスタンスが取れる
    • NSOperationQueueのaddOperationは引数(NSOperationのインスタンス)のmainメソッドをbackgroundで勝手に実行してくれる
iTunesRSSImporter
  • backgroundで処理できるようにNSOperationのサブクラスになってる
iTunesRSSImporter main
  • メモリ使用量を考えて始めにNSAutoreleasePool作って最後にreleaseしている
    • 重い処理やる時はloopの中でautoreleasePool作るって処理と同じだと思われる
キャッシュの話でCategoryCache

// When a managed object is first created, it has a temporary managed object ID. When the managed object context in which it was created is saved, the temporary ID is replaced with a permanent ID. The temporary IDs can no longer be used to retrieve valid managed objects. The cache handles the save notification by iterating through its cache nodes and removing any nodes with temporary IDs.

managed objectがはじめに作られるとき、一時的なmanaged object IDを持っているよ。
managed object contestがsaveされるとき、その一時的なIDは固定IDに変更されるよ。
その一時的なID群はもうmanaged objectを検索できなくなっているよ。

// While it is possible force Core Data to provide a permanent ID before an object is saved, using the method -[ NSManagedObjectContext obtainPermanentIDsForObjects:error:], this method incurrs a trip to the database, resulting in degraded performance - the very thing we are trying to avoid.
- (void)managedObjectContextDidSave:(NSNotification *)notification {
    CacheNode *cacheNode = nil;
    NSMutableArray *keys = [NSMutableArray array];
    for (NSString *key in cache) {
        cacheNode = [cache objectForKey:key];
        if ([cacheNode.objectID isTemporaryID]) {
            [keys addObject:key];
        }
    }
    [cache removeObjectsForKeys:keys];
}