NSURLConnection.sendSynchronousRequestのNSURLSessionでの代替方法

 アプリでちょっとしたWeb上のデータを取得するときにNSURLConnectionのsendSynchronousRequestメソッドを使用していましたが、10.11 El Capitan以降非推奨API(将来的に削除される)となり、代わりにNSURLSessionのdataTaskWithURL:completionHandler:メソッドを使用するようにと警告されるようになりました。

 両者の違いはNSURLConnectionのsendSynchronousRequestが同期型、すなわちデータの取得が完了するかエラーが発生するまで呼び出し元に制御が戻ってこないのに対し、NSURLSessionのdataTaskWithURL:completionHandler:は非同期型で呼び出すとデータの取得を待たずにすぐに制御が戻ってきて、その後データの取得が完了するかエラーが発生すると指定したハンドラーが呼び出されるというものです。

 同期型APIは処理の流れを直線的に記述できるのでプログラミングが簡単なのですが、メインスレッドで呼び出すとデータ取得が完了するまで画面の更新やマウスイベントへの応答ができなくなるので、データ取得に時間がかかるとまるでアプリがハングアップしたように見えます。  一方非同期型APIはそのようなことは起こらなくなるのですが、イベントドリブン型のプログラミングが必要になるのでちょっと面倒くさい。何より、同期型APIを前提に書いた今までのプログラムを非同期型API対応にするのは構造を大きく変える必要があり、そこまで時間を投資するほどのものではないので、NSURLSessionのdataTaskWithURL:completionHandler:で何とか同期型の処理が実現できないか調べてみました。

 まず、NSURLSessionのdataTaskWithURL:completionHandler:の本来の使用方法で実装したテストアプリを走らせてそのログを解析したところ、以下の2点を確認しました。(ログ中の文字列"[4537:3679758]"の':'の左がプロセスIDで右がスレッドIDです)

 ・completionHandlerがdataTaskWithURL:completionHandler:を呼び出したスレッドとは別スレッドで実行されていること
 ・dataTaskWithURL:completionHandler:を呼び出したスレッドをNSThread.sleepForTimeInterval()で強制的に実行を中断させても生成されたNSURLSessionDataTaskは別スレッドで正しく実行されること

【Wrapperクラス】

class UCURLAccessor {
	// Private member variables

	// Properties
	var session: NSURLSession!
	var task: NSURLSessionDataTask?
	var successHandler: (NSData) -> Void = {data in NSLog("Dummy success handler")}
	var errorHandler: (String) -> Void = {errorMessage in NSLog("Dummy error handler")}

	// Initializer
	init() {
		self.session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
		self.task = nil
	}

	// Methods
	// Asynchronous version
	func getContentOfURLAsynchronously(url: NSURL, referer: String? = nil) {
		NSLog("getContentOfURLAsynchronously")
		let req = DMMakeURLRequest(url, referer: referer)
		self.task = self.session.dataTaskWithRequest(req, completionHandler: {data, resp, err in
			NSLog("Completion handler")
			if err == nil {
				let httpResp = resp as! NSHTTPURLResponse
				if httpResp.statusCode == UCDL_HTTP_STATUS_OK || httpResp.statusCode == UCDL_HTTP_STATUS_PARTIAL_CONTENT {
					self.successHandler(data!)
				} else {
					self.errorHandler(self.HTTPStatusCodeToMessage(httpResp.statusCode))
				}
			} else {
				self.errorHandler(err!.localizedDescription)
			}
		})
		self.task!.resume()

		// メインスレッドが実行停止してもdataTaskの実行に影響ないか確認するため意図的にSleep
		NSLog("wait...")
		NSThread.sleepForTimeInterval(2.0)
		NSLog("getContentOfURLAsynchronously exit")
	}
 }

【呼び出し元】
	func test3() {
		self.urlAccessor.successHandler = successHandler
		self.urlAccessor.errorHandler = errorHandler
		let url = NSURL(string: "http://web.dormousesf.com/blog_photos/2016/01/19/DSCN9458HL.jpg")!
		NSLog("call getContentOfURLAsynchronously")
		self.urlAccessor.getContentOfURLAsynchronously(url)
		NSLog("return from getContentOfURLAsynchronously")
	}

	func successHandler(data: NSData) {
		NSLog("successHandler: data length=%ld", data.length)
	}

	func errorHandler(errorMessage: String) {
		NSLog("errorHandler: %@", errorMessage)
	}

【実行結果のログ(completionHandlerとそこから呼び出されるsuccessHandlerが別スレッドで実行されている)】
2016-01-23 21:03:26.682 AsyncDLTest[4537:3679758] call getContentOfURLAsynchronously
2016-01-23 21:03:26.682 AsyncDLTest[4537:3679758] getContentOfURLAsynchronously
2016-01-23 21:03:26.683 AsyncDLTest[4537:3679758] wait...
2016-01-23 21:03:26.706 AsyncDLTest[4537:3679899] Completion handler
2016-01-23 21:03:26.707 AsyncDLTest[4537:3679899] successHandler: data length=134627
2016-01-23 21:03:28.683 AsyncDLTest[4537:3679758] getContentOfURLAsynchronously exit
2016-01-23 21:03:28.683 AsyncDLTest[4537:3679758] return from getContentOfURLAsynchronously

 このことから、dataTaskWithURL:completionHandler:を呼び出したスレッドで、completionHandlerが呼び出されるのを待ち合わせることができないか調べてみました。しかしFoundationフレームワークのNSThreadには該当機能なし。NSThreadに代わる新しい並列処理機構OperationQueueにもそれらしきものなし。

 しばらく考えて思いついたのがUNIX系OSのデファクトスタンダードのスレッドAPI、pthreadが使えるかなということでした。仕事で使っていたのでpthreadに自分が求めている機能が存在することは知っていましたし、Mac OS Xのスレッド関連のソフトウェアスタックの基盤にもなっていることも知っていました。ただ、pthreadはC言語のAPIなので、Mac OS Xの新しい開発言語Swiftで使えるが問題。ググってみたところSwiftから直接呼び出すことができることがわかりました。まぁプロジェクトの中でSwiftとC、C++、Objective-Cの混在は可能なのでSwiftから直接呼出せなくてもC言語系列の言語経由で呼出せはするのですけどね。

 そのようなわけでpthreadを使用することでNSURLSessionにて指定したURLのデータを同期型で取得するコードを実現することができました。

【Wrapperクラス(同期バージョン using pthread)】

class UCURLAccessor {
	// Private member variables
	private let mutex: UnsafeMutablePointer
	private let condition: UnsafeMutablePointer
	private let attribute: UnsafeMutablePointer
	private var doneFlag: Bool
	private var returnValue: Bool

	// Properties
	var session: NSURLSession!
	var task: NSURLSessionDataTask?
	var successHandler: (NSData) -> Void = {data in NSLog("Dummy success handler")}
	var errorHandler: (String) -> Void = {errorMessage in NSLog("Dummy error handler")}
	var contentData: NSData?
	var errorMessage: String?
	
	// Initializer
	init() {
		self.session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
		self.task = nil

		self.doneFlag = false
		self.returnValue = false
		self.contentData = nil
		self.errorMessage = nil

		self.mutex = UnsafeMutablePointer.alloc(sizeof(pthread_mutex_t))
		self.condition = UnsafeMutablePointer.alloc(sizeof(pthread_cond_t))
		self.attribute = UnsafeMutablePointer.alloc(sizeof(pthread_mutexattr_t))

		pthread_mutexattr_init(self.attribute)
		pthread_mutex_init(self.mutex, self.attribute)
		pthread_cond_init(self.condition, nil)
	}

	// Deinitializer
	deinit {
		pthread_cond_destroy(self.condition)
		pthread_mutexattr_destroy(self.attribute)
		pthread_mutex_destroy(self.mutex)
	}
	
	// Methods
	// Asynchronous version
	func getContentOfURLAsynchronously(url: NSURL, referer: String? = nil) {
		省略
	}

	// Synchronous version (Using pthread)
	func getContentOfURLSynchronously(url: NSURL, referer: String? = nil) -> Bool {
		NSLog("getContentOfURLSynchronously")
		self.contentData = nil
		self.errorMessage = nil

		let req = DMMakeURLRequest(url, referer: referer)
		self.task = self.session.dataTaskWithRequest(req, completionHandler: {data, resp, err in
			NSLog("Completion handler")
			if err == nil {
				let httpResp = resp as! NSHTTPURLResponse
				if httpResp.statusCode == UCDL_HTTP_STATUS_OK || httpResp.statusCode == UCDL_HTTP_STATUS_PARTIAL_CONTENT {
					self.contentData = data
					self.returnValue = true
				} else {
					self.errorMessage = self.HTTPStatusCodeToMessage(httpResp.statusCode)
					self.returnValue = false
				}
			} else {
				self.errorMessage = err!.localizedDescription
				self.returnValue = false
			}

			// Awake calling thread
			pthread_mutex_lock(self.mutex)
			self.doneFlag = true
			pthread_cond_signal(self.condition)
			pthread_mutex_unlock(self.mutex)
		})
		self.doneFlag = false
		self.task!.resume()

		// Waiting for completion
		NSLog("wait...")
		pthread_mutex_lock(self.mutex)
		while !self.doneFlag {
			pthread_cond_wait(self.condition, self.mutex)
		}
		pthread_mutex_unlock(self.mutex)

		NSLog("getContentOfURLSynchronously exit")
		return self.returnValue
	}
}

【呼び出し元】
	func test4() {
		let url = NSURL(string: "http://web.dormousesf.com/blog_photos/2016/01/19/DSCN9458HL.jpg")!
		NSLog("call getContentOfURLSynchronously")
		let result = self.urlAccessor.getContentOfURLSynchronously(url)
		NSLog("return from getContentOfURLSynchronously")
		if result {
			NSLog("SUCCESS: data length=%ld", self.urlAccessor.contentData!.length)
		} else {
			NSLog("ERROR: %@", self.urlAccessor.errorMessage!)
		}
	}

【実行結果のログ】
2016-01-23 21:02:10.799 AsyncDLTest[4474:3669252] call getContentOfURLSynchronously
2016-01-23 21:02:10.800 AsyncDLTest[4474:3669252] getContentOfURLSynchronously
2016-01-23 21:02:10.800 AsyncDLTest[4474:3669252] wait...
2016-01-23 21:02:10.802 AsyncDLTest[4474:3672246] Completion handler
2016-01-23 21:02:10.802 AsyncDLTest[4474:3669252] getContentOfURLSynchronously exit
2016-01-23 21:02:10.802 AsyncDLTest[4474:3669252] return from getContentOfURLSynchronously
2016-01-23 21:02:10.802 AsyncDLTest[4474:3669252] SUCCESS: data length=134627


サンプルプロジェクトのダウンロード

【作成・確認環境】
Mac OS X v10.11.3
Xcode v7.2


一覧に戻る