読者です 読者をやめる 読者になる 読者になる

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Blocksを使ったHTTPリクエスト

iOS mixi Objective-C

聖闘士星矢Ωが、思ったより面白くて小宇宙が軽く爆発しそうなk_kinukawaです。
今回は、iOSアプリでHTTP通信を行うときの話です。

2012年4月27日 「メインスレッド上で処理している」について一部修正

従来のNSURLConnectionは、レスポンスをdelegateでハンドリングしていました。
そのため、リクエストを投げる箇所とレスポンスを受ける箇所がコード上で離れてしまい、可読性がよくありませんでした。
また、レスポンスを受け取ったあとの処理についても、delegate内で条件分けをして処理をしているうちに分岐/ネスト地獄になりがちでした。

一方、iOS5からNSURLConnectionにsendAsynchronousRequest:queue:completionHandler:というメソッドが誕生しました。
引数を見る限り、GCDを使って非同期リクエストをする系のメソッドのように見えます。
こいつは便利そうですね!!
しかし、利用可能なのはiOS5以降。iOS4をサポートしている限りこれをそのまま使うことは出来ません><
少し前なら「ネットワーク周りめんどくさい!ASIHTTPRequest使えばいいじゃん!」という流れだったのですが、残念ながらサポート終了されるみたいです
(↑URLがcool)

mixiでは、iOS4でもBlocksを使ってHTTPレスポンスを非同期にハンドリングできるMixiAsyncURLConnectionというクラスを作り、実際に公式クライアントアプリ内で利用しています。
今回は、MixiAsyncURLConnectionの簡易版(でも必要機能はそろっている)をサンプルコードの形にまとめました。

使い方

#import "MixiAsyncURLConnection.h"
 
//1.リクエスト作成
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mixi.jp/"]];
 
//2.MixiAsyncURLConnectionオブジェクト作成
MixiAsyncURLConnection * connection = [[[MixiAsyncURLConnection alloc]initWithRequest:req timeoutSec:10.0f completeBlock:^(id connection, NSData *data){
    //4.レスポンスハンドリング
    self.textView.text = [[[NSString alloc]initWithData:data encoding:NSJapaneseEUCStringEncoding]autorelease];
} progressBlock:nil errorBlock:nil] autorelease];
 
//3.リクエスト実行
[connection performRequest];

簡単ですね。
順に説明します。

1.リクエスト作成

普通にNSURLRequestを作成します。

NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:3000/"]];
[req setHTTPMethod:@"POST"];
[req setHTTPBody:[@"data=hogefuga" dataUsingEncoding:NSUTF8StringEncoding]];

のようにすれば、GETだけでなくPOSTリクエストも作成できます。

2.MixiAsyncURLConnectionオブジェクト作成

MixiAsyncURLConnectionクラスのイニシャライザでは、以下のパラメータを指定できます。

  • Request – NSURLRequestオブジェクトです
  • timeoutSec – タイムアウト時間を指定します
  • completeBlock – レスポンス受信完了時に実行されるBlockを指定します
  • progressBlock – 途中経過をハンドリングするBlockを指定します
  • errorBlock – エラー発生時に実行されるBlockを指定します

3.リクエスト実行

生成されたオブジェクトのperformRequestメソッドを実行すると、リクエストが飛びます。

4.レスポンスハンドリング

レスポンス受信完了すると、非同期でBlockが呼び出されます。
このBlockの中で、受信データをハンドリングします。
もしリクエストキャンセルをしたい場合は、MixiAsyncURLConnectionオブジェクトのcancelメソッドを呼びます。
とても簡単ですね。

特徴

いくつか注目ポイントがあります。

タイムアウト

NSURLRequestのrequestWithURL:cachePolicy:timeoutInterval:を使うと、リクエストのタイムアウトを指定できますが、実は落とし穴があります。
POSTリクエストをする時に、NSMutableURLRequestのsetHTTPMethodメソッドを使うと、タイムアウトがなぜか効かなくなってしまいます。
色々調べた結果、このような情報を見つけました。残念ですね。
そこで、MixiAsyncURLConnectionでは自前のタイムアウト処理を入れてあります。
これで、POSTリクエストでも自由にタイムアウト時間を設定できます。

Blocksによる非同期処理

completeBlock、progressBlock、errorBlockによって、様々な状態をハンドリングできます。
progressBlockを使えば、写真アップロードの途中経過をプログレスバーにリアルタイムで表示するような処理が簡単に書けます。
errorBlockではNSErrorオブジェクトが返ってくるので、リクエストごとにエラーハンドリングを記述することができます。
注意点としては、errorBlockにはiPhoneの通信エラーのようなNSURLConnectionのエラーが返ってきます。
HTTPのレスポンスコード400番台や500番台はcompleteBlockで受け取ります。

メインスレッド上で処理している

GCDとかNSOperationを使った並列ネットワーク処理のサンプルは、調べるといくつも発見することができます。
しかし、WWDCの動画等を見ていると、UIの処理はもちろん、ネットワーク処理もメインスレッド上で行うべきだとの意見もあります。
上の表記についてご指摘を頂きました。下に詳細を追記します。

MixiAsyncURLConnectionでは、GCDもNSOperationも利用していません。
もしレスポンスのハンドリングが重い処理になる時には、completeBlockの中でGCDを使ってタスクキューへ処理を投げてしまうと良さ気です。

追記
WWDCの動画では、「ネットワーク処理もメインスレッド上で行うべき」とは言っていませんでした。
該当する動画はこちらです。
iOS developerのアカウントでログインすると視聴できます。
WWDC2010 video
Network Apps for iPhone OS, Part 2

また、ご指摘くださったniwさんに素晴らしい調査レポートブログ記事を紹介していただきました。
とても勉強になります。ありがとうございました。

終わりに

実はMixiAsyncURLConnectionの中身は、たった100行ちょっとのNSURLConnectionクラスの拡張です。
しかし、たったこれだけで非常に使いやすいクラスに化けてくれます。
ひと通り動作確認、メモリリークチェックはしています。
機能も最小限なので、ここから自分好みに拡張していくのも楽しいのではないでしょうか。