异步问题

在讨论处理异步前,不如来思考一下为什么需要异步的代码。

let imageURL = NSURL(string: "https://github.com/fluidicon.png")!
let data = NSData(contentsOfURL: imageURL)!
let image = UIImage(data: data)
self.imageView.image = image

SyncDownloadImageViewController.swift

这是一段加载图片的代码,也很好理解,用图片描述就是:

通过一个 URL 获取下载到的 Data ,然后通过 Data 生成 UIImage ,最后设置到对应的 UIImageView 上。

很清晰的一段代码,但是现实中我们不可能这样写,很简单的道理,这样的代码会卡住主线程,导致极度不好的用户体验,此外长时间的阻塞主线程还会导致程序退出。

这段代码在网络环境稍差的情况下,点击 Button ,Button 会一直卡在按下的场景很久,直到图片加载完毕。

因此,在和网络进行交互时,往往将下载过程扔到其他线程来做,而且为了使用更方便,往往是选择对 NSURLSession 进行封装后使用,知名的第三方网络库就是 Alamofire 。这里笔者暂时使用 NSURLSession 进行网络请求。

let imageURL = NSURL(string: "https://github.com/fluidicon.png")!
NSURLSession.sharedSession()
.dataTaskWithURL(imageURL) { (data, _, _) in
let image = UIImage(data: data!)
dispatch_async(dispatch_get_main_queue()) {
self.imageView.image = image
}
}
.resume()

AsyncDownloadImageViewController.swift

这段代码理解起来就稍微复杂一些了,但也没有那么复杂。通过 NSURLSession 加载 Data ,Data 以闭包形式传递回来,根据 Data 生成 UIImage ,切换到主线程,设置图片到 UIImageView 。

事实上这段代码和上面同步加载图片的代码逻辑是相近的,都是一个从 URL 到 Data 再到 UIImage 的过程,唯一多了的就是线程切换。但是这段代码的层级结构并没有描述清楚这一过程,这是一个回调地狱,随着我们的异步代码越来越多,闭包嵌套也就更加严重,代码可读性成指数级下降。

这里面临的两个问题是异步代码线程切换。这些都影响了代码的原有结构,而 RxSwift 可以很好、很优雅的解决异步和线程切换问题

先来看一下使用 NSData(contentsOfURL: imageURL)! 同步加载的这段代码如何完成,我们可以看到 Rx 下的线程切换是多么的方便:

.observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))

切换到后台线程

.observeOn(MainScheduler.instance)

切换到主线程

很简单,调用 observeOn 方法,在参数中指定具体的线程,后面的操作变换都会在该线程中进行:

.map { return "https://github.com/fluidicon.png" }
.map { rawURLString in return NSURL(string: rawURLString)! }
.observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.map { url in return NSData(contentsOfURL: url)! } // 在后台执行
.map { data in return UIImage(data: data)! } // 在后台执行
.observeOn(MainScheduler.instance)

DownloadImageWithRxAndDataViewController.swift

这段代码的详细流程如下:

这样就很清晰了,每一个 map 和 observeOn 都描述了一个很小的操作,然后将这些操作按照需要的逻辑顺序拼接起来。

此时再去点击 Button ,UI 便不会卡在 Button 按下状态了。

然而我们更可能使用 NSURLSession 请求数据:

extension NSURLSession {
public func dataTaskWithRequest(request: NSURLRequest, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask
}

传入一个 (NSData?, NSURLResponse?, NSError?) -> Void 闭包,在数据请求完毕后,调用这个闭包,需要注意的是这个闭包并非执行在主线程。

来看用 RxSwift 和 NSURLSession 如何完成上面这段代码逻辑,Rx 已经为我们做好了封装:

extension NSURLSession {
public func rx_response(request: NSURLRequest) -> RxSwift.Observable<(NSData, NSHTTPURLResponse)>
public func rx_data(request: NSURLRequest) -> RxSwift.Observable<NSData>
}

然而:

.map { return "https://github.com/fluidicon.png" }
.map { rawURLString in return NSURL(string: rawURLString)! }
.map { url in return NSURLRequest(URL: url) }
.map { urlRequest in
NSURLSession.sharedSession().rx_data(urlRequest)
}
.map(selector: Observable<NSData> throws -> Observable<NSData> throws -> R)

在调用了 rx_data 这个方法后,接下来传递的参数变成了 Observable<NSData> 。但事实上就是这个参数:

通过变换,传递了一个传递 Observable 的 Observable 。

flatMap 可以帮我们解决这个问题:

.map { return "https://github.com/fluidicon.png" }
.map { rawURLString in return NSURL(string: rawURLString)! }
.map { url in return NSURLRequest(URL: url) }
.flatMap { urlRequest -> Observable<NSData> in
return NSURLSession.sharedSession().rx_data(urlRequest)
}
.map(selector: NSData throws -> R)

类似于 Swift 中的 flatMap ,Rx 中的 flatMap 也有一个打平(解包)的效果,传入的闭包是返回一个 Observable ,在内部取出这些 Observable 中传递的值,组合到一起传递下去。上面的代码就是取出每个 Observable 中的 Data 传递下去。

完整代码如下:

downloadButton.rx_tap
.map { return "https://github.com/fluidicon.png" }
.map { rawURLString in return NSURL(string: rawURLString)! }
.observeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.map { url in return NSData(contentsOfURL: url)! }
.map { data in return UIImage(data: data)! }
.observeOn(MainScheduler.instance)
.subscribeNext { [unowned self] image in
self.imageView.image = image
}
map 和 flatMap 的一点使用上的区别是
map 只能同步的传递、变换数据
flatMap 可以既可以同步的处理数据,也可以异步的处理数据

我们将在下一节 Iterable 和 Observable 中继续讨论处理异步的问题。