【Alamofireを読む】メソッドチェインと遅延実行の実装

こんにちは。アプリケーションエンジニアのid:yashigani_wです。 この記事は、はてなエンジニアアドベントカレンダー2014の12日目の記事です。

Swiftの登場から約半年。 はてなでは、徐々にSwiftへの移行を進めています。 今回はSwiftでHTTP通信を簡単に実装することができる、Alamofireというライブラリの実装についてのお話です。

AlamofireはObjective-CのHTTP通信ライブラリとして圧倒的支持を誇る、AFNetworkingの作者であるMattt Thompson氏によるプロダクトです。 いち早くSwiftに対応したものであるということだけでなく、簡潔な記述ができることもあって注目を集めています。 このAlamofireを使うと、通信の処理を以下のように書くことができます。

Alamofire.request(.GET, "http://b.hatena.ne.jp/entry/json/?url=http%3A%2F%2Fdeveloper.hatenastaff.com%2Fentry%2F2014%2F12%2F01%2F164046")
.response { (_, response, _, _) in
  println(response.statusCode)
}
.responseString { (_, _, string, _) in
  println(string)
}
.responseJSON { (_, _, JSON, _) in
  println(JSON)
}

メソッドチェインで遅延実行されるコールバックのclosureを書き連ねることができ、モダンな雰囲気がします。 しかし、これは一体どのように実装されているのでしょうか? 内部でclosureを保持しているのだろうという予想はできますが、いたってシンプルな実装になっておりおもしろいです。

メソッドチェイン・遅延実行の秘密に迫る!

AlamofireではひとつのHTTPのリクエストに対してRequestを作成します。 RequestNSURLSessionTaskRequest.TaskDelegateを持ち、Request.TaskDelegateNSURLSessionTaskDelegateを実装しています。

RequestresponseStringresponseJSONの処理は最終的にresponseメソッドに集約します。 ということで早速responseメソッドの実装を見てみましょう。

public func response(queue: dispatch_queue_t? = nil, serializer: Serializer, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self {
    // `Request.TaskDelegate`の`queue`にコールバックの実行を登録
    dispatch_async(delegate.queue) {
        let (responseObject: AnyObject?, serializationError: NSError?) = serializer(self.request, self.response, self.delegate.data)

        dispatch_async(queue ?? dispatch_get_main_queue()) {
            completionHandler(self.request, self.response, responseObject, self.delegate.error ?? serializationError)
        }
    }

    return self
}

Alamofire/Alamofire.swift at 445a4ec908aa2b44b13602b2b71e99378084293f · Alamofire/Alamofire · GitHub

当然ですが、responseメソッドはRequestオブジェクト自身を返すのでメソッドチェインを使って処理を書き連ねることができます。 注目したいのはdelegate.queueに、completionHandlerを呼び出すclosureを登録する処理です。 どうやらこれが遅延実行の秘密です。

さらに秘密を紐解く

では、delegate.queueとはどのようなものなんでしょうか? delegateの正体は、Request.TaskDelegateというNSURLSessionTaskDelegateを実装したオブジェクトでした。 Request.TaskDelegatequeueに迫ってみると、以下のようになっています。

class TaskDelegate: NSObject, NSURLSessionTaskDelegate {
    let task: NSURLSessionTask
    let queue: dispatch_queue_t

    // 省略...

    init(task: NSURLSessionTask) {
        self.task = task
        self.progress = NSProgress(totalUnitCount: 0)
        // `dispatch_queue`を生成し、suspendする
        self.queue = {
            let label: String = "com.alamofire.task-\(task.taskIdentifier)"
            let queue = dispatch_queue_create((label as NSString).UTF8String, DISPATCH_QUEUE_SERIAL)

            dispatch_suspend(queue)

            return queue
        }()
    }
}

Alamofire/Alamofire.swift at 445a4ec908aa2b44b13602b2b71e99378084293f · Alamofire/Alamofire · GitHub

なんてことはなく、ただのdispatch_queueです。 しかし、イニシャライザでdispatch_queueを作ったと同時にsuspendしているのがなんとも意味深です。 これを踏まえて、通信の終了時の処理であるURLSession(session: NSURLSession!, task: NSURLSessionTask!, didCompleteWithError error: NSError!)の実装を探してみましょう。

Alamofireでは、NSURLSessionManagerが保持し、NSURLSessionDelegate/NSURLSessionTaskDelegateManager.SessionDelegateに実装されています。 Manager.SessionDeleagteURLSession(session: NSURLSession!, task: NSURLSessionTask!, didCompleteWithError error: NSError!)taskに紐付いたRequestオブジェクトを取り出し、そのdelegateに終了を通知します。

ややこしいですが、つまるところRequest.TaskDelegateURLSession(session: NSURLSession!, task: NSURLSessionTask!, didCompleteWithError error: NSError!)の実装を見ればいいということです。

func URLSession(session: NSURLSession!, task: NSURLSessionTask!, didCompleteWithError error: NSError!) {
    if error != nil {
        self.error = error
    }
    // 通信が終了したのでsuspendしていたqueueをresume
    dispatch_resume(queue)
}

Alamofire/Alamofire.swift at 445a4ec908aa2b44b13602b2b71e99378084293f · Alamofire/Alamofire · GitHub

suspendしていたqueueをresumeしています。 つまり、suspendしているqueueにclosureを登録し、通信終了時にresumeすることで遅延実行していたんですね。 普通にやるとclosureの保持は複雑で嫌な実装になりそうですが、GCDをうまく使うことで驚くほど簡単に実装されています!

まとめ

AlamofireはAFNetworkingのMatttさんのプロダクトということもあり、便利なライブラリというだけでなくコードからも多くのことを学ぶことができます。 今回紹介したコールバックの遅延実行以外にもおもしろい実装がたくさんあります。 AFNetworkingと比べ実装も小さなものになっていますので年末年始の暇な時間にコードリーディングしてみてはいかがでしょうか。

明日はid:stanakaさんです。

参考リンク

素敵なお知らせ

Swiftでアプリを開発したいエンジニアの方はこちらからエントリーしてください!

iPhone、Androidアプリエンジニア職 - 株式会社はてな