2010年11月23日

NSURLResponseのexpectedContentLengthの使い方

環境iOS SDK 4.0

NSURLConnectionを使ってデータをダウンロードするとき、 ダウンロードデータを格納するNSMutableDataの容量をダウンロードデータのサイズと合わせておこうと思って NSURLConnectionのdelegateを担うクラスに以下のようなコードを書きました。

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
  long long length = response.expectedContentLength;
  receivedData = [[NSMutableData alloc] initWithCapacity:length];
}

そうしたところ、NSMutableDataのinitWithCapacity:が例外で落ちました。 response.expectedContentLengthに-1が入っているようです。
HTTP Responseからサイズを取得できないときはそうなるようですが、 今時サイズを取得できないようなResponseを返すサーバがあるのかなと思って調べてみました。

HTTPのRequestとResponseをキャプチャしたかったので、横取り丸 1.98InetSpy 1.1を使って、HTTPをモニタできる環境を整え、 iPodのプロキシ設定を変更してRequestとResponseをキャプチャしてみました。
なお、iPhone Simulatorでプロキシ設定を変更する方法は分からなかったので、実機でしか確認できませんでした。

結果、Responseに以下のEntityがあることが分かりました。

Content-Encoding: gzip

そりゃ圧縮されていたらサイズ分かりませんね・・・。 今時のサーバでは圧縮はよくある話なのでresponse.expectedContentLengthに-1が入るのは納得しました。

connection:didReceiveResponse:は以下のように書き直しました。

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
  const int MAX_INIT_SIZE = 100 * 1024 * 1024;
  long long length = response.expectedContentLength;
  if (length < 0 || length > MAX_INIT_SIZE) {
    length = MAX_INIT_SIZE;
  }
  receivedData = [[NSMutableData alloc] initWithCapacity:length];
}

response.expectedContentLengthに-1が入ってきた場合にも対応し、 さらに上の方も100KBで制限をかけることにしました。いきなり大きなサイズにするのも気がひけたからです。
これでデータのダウンロードもスムーズになるのではないかと思います。(本当になるかどうかは確認できませんでしたが)

以下はおまけですが、InetSpyでは、gzipで圧縮されたファイルしか取れないため、展開後のデータを確認できません。
確認するためにPerlスクリプトを作成しましたので、参考までにコードを置いておきます。

# 参考コード: http://www.tohoho-web.com/perl/encode.htm
use strict;
use Compress::Zlib;

die "Usage: gzip [IN_FILE] [OUT_FILE]\n" if ($#ARGV < 1);
my $infile = $ARGV[0];
my $outfile = $ARGV[1];
my $indata;
my $outdata;

open(IN, $infile) || die "Can't open $infile.\n";
binmode(IN);
read(IN, $indata, (lstat(IN))[7]) || die "Can't read $infile\n";
close(IN);

open(OUT, "> $outfile") || die "Can't open $outfile.\n";
binmode(OUT);
print OUT Compress::Zlib::memGunzip($indata);
close(OUT);