THttpClient の落とし穴１


これは [Delphi Advent Calendar](http://qiita.com/advent-calendar/2015/delphi) 補欠の記事です。

#THttpClient とはなんぞや

[THttpClient](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Net.HttpClient.THTTPClient) は [System.Net.HttpClinet ユニット](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Net.HttpClient)に含まれる、[新しい HTTP クライアント](http://docwiki.embarcadero.com/RADStudio/XE8/ja/Delphi_XE8_%E3%81%8A%E3%82%88%E3%81%B3_C%2B%2BBuilder_XE8_%E3%81%AE%E6%96%B0%E6%A9%9F%E8%83%BD#.E6.96.B0.E3.81.97.E3.81.84_HTTP_.E3.82.AF.E3.83.A9.E3.82.A4.E3.82.A2.E3.83.B3.E3.83.88_API)です。

旧来の Indy TIdHttp は Indy が全ての OS で動くように独自に実装していますが、THttpClient クラスは、それぞれの OS が提供している HTTP ライブラリを使います。
そのため、高速かつ堅牢です（メンテナンスは OS 側に任せられる）。

また、Indy で SSL を使おうとすると [OpenSSL ライブラリが必要](http://support.embarcadero.com/jp/article/43662)だったりして、結構面倒でした。
THttpClient クラスは、追加のライブラリはもちろん必要ありません！

普段は非ビジュアルコンポーネントとして THttpClient をラップした [TNetHttpClient コンポーネント](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Net.HttpClientComponent.TNetHTTPClient)を使う方が多いかもしれませんが、今回は THttpClient を直接使って、画像を読み込んでみます。

#画像の取得

HTTP 経由での画像取得は、Get メソッドを呼び出すだけなのですが、THttpClient はブロッキング動作になるので、一度スレッドで囲んであげます。

今回は実は画像の取得がメインではないので、[TThread.CreateAnonymousThread](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Classes.TThread.CreateAnonymousThread) を使って簡単に済ませました。
↓たったこれだけのコードで画像を取得できます。超簡単ですね！！

```delphi
const
  URL = 'https://qiita-image-store.s3.amazonaws.com/0/12977/af4a05d5-dea6-979e-c367-d80241056406.png';

TThread.CreateAnonymousThread(
  procedure
  var
    Http: THttpClient;
    FS: TFileSTream;
    FileName: String;
  begin
    FileName := TPath.Combine(TPath.GetDocumentsPath, 'test.jpg');

    // 画像の取得
    FS := TFileStream.Create(FileName, fmCreate, $1ff); // $1ff -> rwxrwxrwx
    try
      Http := THttpClient.Create;
      try
        Http.HandleRedirects := True;
        Http.OnReceiveData := HttpReceiveData;

        Http.Get(URL, FS);
      finally
        Http.DisposeOf;
      end;
    finally
      FS.DisposeOf;
    end;

    // 画像の表示
    TThread.Synchronize(
      TThread.Current,
      procedure
      begin
        Image1.Bitmap.LoadFromFile(FileName);
      end
    );
  end
).Start;
```
Android での実行例
![de.png](https://qiita-image-store.s3.amazonaws.com/0/12977/44db8af4-d847-f103-183d-00439dfdbb0d.png)

うん。簡単！！

#トラブル発生
Android 5.x, Android 6.x 端末で動作確認していたのですが…！
Android 4.x で動かすと…

![de2.png](https://qiita-image-store.s3.amazonaws.com/0/12977/2b38a4c9-0fd6-34e1-e84f-9012e3aa3aee.png)

何も表示されていない！！！
しかも FileSize が 0 byte ！

#問題は TJavaArray に
Dalivk から ART に実行環境が変わったときに TJavaArray の扱いが変わりました。
これを [Fix した時](http://qc.embarcadero.com/wc/qcmain.aspx?d=129208)に、Android 4.x 系（Dalvik）での調査が不十分だったのかなあと勝手に勘ぐっております。

問題が結構深い所にあるので、[とりあえず QP に報告した](https://quality.embarcadero.com/browse/RSP-13182)ものの、このままでは困ります。

ただ、最終的な問題箇所は [System.SysUtils.FileWrite](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.SysUtils.FileWrite) の型無し引数版です。
なので、これを型あり引数版を使うようにしてしまえば良いのです！

ということで[修正するユニット](https://gist.github.com/freeonterminate/8579d4ac5142efbe77a4)を作りました。

#具体的な修正ポイント

THttpClient からは [TStream.WriteData メソッド](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Classes.TStream.WriteData)が呼び出されますが、最終的には [TStream.Write メソッド](http://docwiki.embarcadero.com/Libraries/Seattle/ja/System.Classes.TStream.Write)が呼ばれます。
ですので、TFileStream.Write を override して下記の様に書き換えてやります。

```delphi
type
  TFileStream = class(System.Classes.TFileStream)
  public
    function Write(
      const Buffer: TBytes;
      Offset, Count: Integer): Longint; override;
  end;

implementation

function TFileStream.Write(
  const Buffer: TBytes;
  Offset, Count: Integer): Longint;
begin
  Result := FileWrite(Handle, Buffer[Offset], Count);
end;
```
この Unit を uses するだけで、THttpClient.Get に TFileStream を渡しても、正しく動作するようになりました。

#サンプルアプリ
上記で使ったサンプルアプリを [GitHub にアップ](https://github.com/freeonterminate/delphi/tree/master/HttpClientSample)しました。
[Get image] ボタンをタップすると「でるお」の画像をダウンロードしてきます。

#まとめ
実は THttpClient.Get メソッドは第二引数を指定しなければ、TMemoryStream が自動的に生成され THttpClient.Get の戻り値の [IHttpResponse.ContentStream](http://docwiki.embarcadero.com/Libraries/XE8/ja/System.Net.URLClient.IURLResponse.ContentStream) からデータを取得できます。
例えば今回のように画像などの小さなデータであれば、メモリ上に読み込んでも問題ありません（しかも、ファイルに書かないから速い！）。
ですが、ゲームに使うようなリソースファイル一式とかの大きなデータの場合、メモリが足りずに失敗してしまいます。
なので、TFileStream を第二引数に指定できるようになっているのですが、Android 4.x では動作しなかったので困ったね、というお話でした。


#落とし穴２
[つづくよ！！](http://qiita.com/items/1361696efeb4348f2acc)