Grand Central Dispatchで楽々マルチスレッド iPhoneプログラミング
iPhoneでもiOS4以降サポートされたGrand Central Dispatchを使うと、マルチスレッドを使ったプログラムが簡単に作れます。WWDC 2010のビデオ(#206, #211) を見て何となく判った気になったのですが、まだクリアでない点があったので自分でコードを書いてみました。
私の理解では、Grand Central Dispatch(GCD)はには
- マルチプロセッサを有効に使える、並列プログラムを簡単に書ける
- 操作性を高める、並行処理を簡単に書ける
の2つの目的があると思います。ここでは 2. に付いて書きます。 1.については Wikipediaの The second exampleが参考になると思います。
今回のサンプル
ここでは、Twitterのpublic 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 上で
- getPublicTimelineでtimeline取得
- 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キュー上で
- getImageでアイコン画像の取得
- 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を使ったコード」です。