Grand Central Dispatchで楽々マルチスレッド iPhoneプログラミング

iPhoneでもiOS4以降サポートされたGrand Central Dispatchを使うと、マルチスレッドを使ったプログラムが簡単に作れます。WWDC 2010のビデオ(#206, #211) を見て何となく判った気になったのですが、まだクリアでない点があったので自分でコードを書いてみました。

http://upload.wikimedia.org/wikipedia/en/b/bd/Gcd_icon20090608.jpg

私の理解では、Grand Central Dispatch(GCD)はには

  1. マルチプロセッサを有効に使える、並列プログラムを簡単に書ける
  2. 操作性を高める、並行処理を簡単に書ける

の2つの目的があると思います。ここでは 2. に付いて書きます。 1.については Wikipediaの The second exampleが参考になると思います。

今回のサンプル


ここでは、Twitterpublic timelineを取得し、つぶやきとアイコンを表示するプログラムを、スレッドをまったく使わないコードとGCDを使ったコードを比較してみようと思います。








.

スレッドをまったく使わないコード

処理は 画面が表示された時に、public timelineのJSONデータを取得、TableViewの表示、1つのセル表示の中でのアイコンの取得を全て同期処理しています。JSONのパースはJSON Frameworkを使っています。

したがってtimelineが表示されるまでは、3G通信の実機では5秒くらい真っ黒な画面が表示されたままですし、スクロール時にアイコンを取得するのでスクロールの反応は最悪です ^^);
(Wifi等でネットに繋がったMac上のシミュレータでは動作が早いですが、ネットワークの通信速度を制限する Preference Pane "SpeedLimit" - 24/7 twenty-four seven を参考に、通信速度を落としてみると判ります)

- (NSData *)getData:(NSString *)url; {
    NSURLRequest *request = [NSURLRequest requestWithURL:
                             [NSURL URLWithString:[url stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]
                             cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30.0];
    NSURLResponse *response;
    NSError       *error;
    NSData *result = [NSURLConnection sendSynchronousRequest:request 
                                           returningResponse:&response error:&error];
    if (result == nil) {
        NSLog(@"NSURLConnection error %@", error);
    }
    
    return result;
}

- (UIImage *)getImage:(NSString *)url {
    return [UIImage imageWithData:[self getData:url]];
}

- (void)getPublicTimeline {
    NSString *jsonString = [[[NSString alloc] initWithData:[self getData:TWITTER_URL_PUBLIC_TIMELINE]
                                         encoding:NSUTF8StringEncoding] autorelease];
    NSArray *entries = [jsonString JSONValue];

    self.tweetMessages = [[NSMutableArray alloc] init];
    self.tweetIconURLs    = [[NSMutableArray alloc] init];
    for (NSDictionary *entry in entries) {
        [tweetMessages addObject:[entry objectForKey:@"text"]];
        [tweetIconURLs addObject:[[entry objectForKey:@"user"] objectForKey:@"profile_image_url"]];
    }
}


- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    [self getPublicTimeline];
    [self.tableView reloadData];
}


- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 66.0;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return tweetMessages ? [tweetMessages count] : 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"SampleTable";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
    }
    cell.textLabel.text = [tweetMessages objectAtIndex:[indexPath row]];
    cell.textLabel.adjustsFontSizeToFitWidth = YES;
    cell.textLabel.numberOfLines = 3;
    cell.textLabel.font = [UIFont boldSystemFontOfSize:12];
    cell.imageView.image = [self getImage:[tweetIconURLs objectAtIndex:[indexPath row]]];
    
    return cell;
}

Grand Central Dispatchを使ったコード

まず、GCDのキュー用の変数を準備します。
GCDではキューに登録された処理(ブロック)は順番に実行されます。しかしキューを複数作成した場合、キューは別スレッドで実行されるので並行処理が実現できます。したがって、アプリで並行に処理したい部分用のキューを用意します。
今回は、メイン(GUI)用、timeline取得用、アイコン画像取得用の3つのキューを用意しました。

    dispatch_queue_t main_queue;
    dispatch_queue_t timeline_queue;
    dispatch_queue_t image_queue;

画面が表示されたところで、メインスレッド以外のスレッドを作成しています。メイン(GUI)用キューは dispatch_get_main_queue() で取得します。

さて、timeline取得ですが timeline_queue 上で

  1. getPublicTimelineでtimeline取得
  2. tableViewの再表示処理をmain_queueに登録

の順に実行されるように、timeline_queueに登録します。これで timeline取得が別スレッドで実行され、その後メインスレッドで再表示が実行されます。上の 「スレッドをまったく使わないコード」 にほんの少しのGCDコードを追加しただけの事が判ると思います。
また、この処理はキューにブロックを登録するだけなので直ぐに終了しますので、直ぐに空のtableViewが表示されます。

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    main_queue = dispatch_get_main_queue();
    timeline_queue = dispatch_queue_create("com.ey-office.gcd-sample.timeline", NULL);
    image_queue = dispatch_queue_create("com.ey-office.gcd-sample.image", NULL);
    
    dispatch_async(timeline_queue, ^{
        [self getPublicTimeline];
        dispatch_async(main_queue, ^{
            [self.tableView reloadData];
        });
    });
}


次に tableViewの表示ですが、最初に表示される時に tableView に loading... と表示されるようにしています。

さて、timeline取得後の再表示ではアイコン画像の取得・表示をGCDで実行しています。ここでは image_queueキュー上で

  1. getImageでアイコン画像の取得
  2. tableViewのCellに画像を設定する処理を main_queueに登録

を行い、アイコン画像の取得・表示を別スレッドで行います。やはりここでもキューにブロックを登録するだけなので直ぐにtableViewにつぶやきとblankアイコンが表示されます。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return tweetMessages ? [tweetMessages count] : 1;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"SampleTable";
    
    if (!tweetMessages) {
        UITableViewCell *cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
        cell.textLabel.text = @"loading...";
        cell.textLabel.textColor = [UIColor grayColor];
        return cell;
    }
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
    }
    cell.textLabel.text = [tweetMessages objectAtIndex:[indexPath row]];
    cell.textLabel.adjustsFontSizeToFitWidth = YES;
    cell.textLabel.numberOfLines = 3;
    cell.textLabel.font = [UIFont boldSystemFontOfSize:12];
    cell.textLabel.textColor = [UIColor darkGrayColor];
    cell.imageView.image = [UIImage imageNamed:@"blank.png"];

    dispatch_async(image_queue, ^{
        UIImage *icon = [self getImage:[tweetIconURLs objectAtIndex:[indexPath row]]];
        dispatch_async(main_queue, ^{
            UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
            cell.imageView.image = icon;
        });
    });

    return cell;
}

最後に画面終了時にキューを削除しています。

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    dispatch_release(timeline_queue);
    dispatch_release(image_queue);
}

まとめ

GCDを使ったコードでは起動後直ぐに テーブルに Loading...が表示され、少し待つとつぶやきが表示され、アイコンも順番に表示されていきます。しかも、テーブルをいつでもスクロール出来ます。

通常、スレッドプログラムでは各スレッドの同期や、共有リソース(変数)の排他制御はたいへんです、どのくら大変なのかは Java並行処理プログラミング ―その「基盤」と「最新API」を究める― を眺めると解ります ^^;
iOSではNSOperationを使うかなり楽に書けるようになりますが、スレッドを使わないコードからの書き換え量はかなりあります。


しかし、GCDを使うと利用者にストレスを与えない反応の良いアプリを、簡単に安全に作る事ができます! iOS3に対応しなくても良いアプリでは、どんどんGCDを使うべきだと思います。
GCDの詳細は WWDC 2010のビデオ の#206, #211が大変参考になります。

今回のコードはGitHubにアップしてあります。no_thread ブランチが 「スレッドをまったく使わないコード」で gcd ブランチが 「Grand Central Dispatchを使ったコード」です。