如何在Swift中完成外部异步请求之前先完成内部异步请求?

时间:2016-05-13 05:14:31

标签: ios swift asynchronous

我一直在努力实现这一目标,并且无法让它发挥作用。

首先让我展示一个简单的示例代码:

override func viewDidLoad()
{
    super.viewDidLoad()

    methodOne("some url bring")
}

func methodOne(urlString1: String)
{
    let targetURL = NSURL(string: urlString1)

    let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) {(data, response, error) in

        // DO STUFF
        j = some value

        print("Inside Async1")
        for k in j...someArray.count - 1
        {  
            print("k = \(k)")
            print("Calling Async2")
            self.methodTwo("some url string")
        }

    }

    task.resume()
}

func methodTwo(urlString2: String)
{
    let targetURL = NSURL(string: urlString2)

    let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) {(data, response, error) in

        // DO STUFF
        print("inside Async2")
    }

    task.resume()
}

我基本上做的是在methodOne内执行异步请求,在该函数中,我调用我的methodTwo执行另一个异步请求。

我遇到的问题是,当调用methodTwo时,它永远不会进入异步会话。但是,它在methodTwo内进入异步会话,但只进入k = someArray.count - 1一次。它基本上排队直到最后,这不是我想要实现的目标。

以下是示例输出:

Inside Async1
k = 0
Calling Async2
Inside Async1
k = 0
Calling Async2
k = 1
Calling Async2
Inside Async1
k = 0
Calling Async2
k = 1
Calling Async2
k = 2
Calling Async2
Inside Async1
.....
Inside Async1
k = 0
Calling Async2
k = 1
Calling Async2
k = 2
Calling Async2
k = 3
Calling Async2
k = 4
Inside Async2

换句话说,我希望在methodTwo的异步请求完成之前,每次迭代都会完成methodOne的异步请求。

以下是我的目标的示例输出:

Inside Async1
k = 0
Calling Async2
Inside Async2
Inside Async1
k = 1
Calling Async2
Inside Async2
Inside Async1
...

我在这里发现了类似的内容:Wait until first async function is completed then execute the second async function

但是,我无法使用这些建议和解决方案。

有人能指出我正确的方向吗?

由于

5 个答案:

答案 0 :(得分:1)

您应该使用同步请求。 这个扩展程序很容易使用:

extension NSURLSession {
    public static func requestSynchronousData(request: NSURLRequest, completion: ((data: NSData?, error: NSError?) -> Void)?) {
        var data: NSData? = nil
        var error: NSError? = nil
        let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)
        NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {
            taskData, _, taskError -> () in
            data = taskData
            error = taskError
            if data == nil, let error = error {print(error)}
            dispatch_semaphore_signal(semaphore);
        }).resume()
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
        completion?(data: data, error: error)
    }
}

并在methodTwo中发送同步请求:

func methodOne(urlString1: String) {
    guard let targetURL = NSURL(string: urlString1) else { return }
    let request = NSURLRequest(URL: targetURL)
    NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
        // DO STUFF
        print("Inside Async1")
        for k in 0..<5 {
            print("k = \(k)")
            print("Calling Async2")
            self.methodTwo("http://www.google.com")
        }
    }.resume()
}

func methodTwo(urlString2: String) {
    guard let targetURL = NSURL(string: urlString2) else { return }
    let request = NSURLRequest(URL: targetURL)
    NSURLSession.requestSynchronousData(request) { (data, error) in
        // DO STUFF
        print("inside Async2")
    }
}

您也可以使用调度队列进行管理。 Learn more about GCD

答案 1 :(得分:1)

执行此操作的一种方法是更改​​methodTwo()以接受回调作为参数,然后您可以使用信号量:

func methodOne(urlString1: String) {
    let targetURL = NSURL(string: urlString1)
    let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) { data, response, error in
        let queue = dispatch_queue_create("org.myorg.myqueue", nil)
        dispatch_async(queue) {

            // DO STUFF
            j = some value

            print("Inside Async1")
            for k in j...someArray.count - 1 {  
                print("k = \(k)")

                print("Calling Async2")
                dispatch_semaphore_t sem = dispatch_semaphore_create(0);
                self.methodTwo("some url string") {
                    dispatch_semaphore_signal(sem);
                }
                dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
            }
        }
    }
    task.resume()
}

func methodTwo(urlString2: String, callback: (() -> ())) {
    let targetURL = NSURL(string: urlString2)
    let task = NSURLSession.sharedSession().dataTaskWithURL(targetURL!) { data, response, error in

        // DO STUFF
        print("inside Async2")
        callback()
    }
    task.resume()
}

请注意,为了不阻止methodOne的任务回调的委托队列,该示例创建了自己的队列,您可以随意阻止它。

答案 2 :(得分:1)

而不是其他人已经建议的信号量或组(阻塞线程,如果你有太多的线程被阻塞可能会有问题),我会使用自定义的异步NSOperation子类来处理网络请求。一旦您将请求包装在异步NSOperation中,您就可以将一堆操作添加到操作队列中,而不是阻塞任何线程,而是享受这些异步操作之间的依赖关系。

例如,网络操作可能如下所示:

class NetworkOperation: AsynchronousOperation {

    private let url: NSURL
    private var requestCompletionHandler: ((NSData?, NSURLResponse?, NSError?) -> ())?
    private var task: NSURLSessionTask?

    init(url: NSURL, requestCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> ()) {
        self.url = url
        self.requestCompletionHandler = requestCompletionHandler

        super.init()
    }

    override func main() {
        task = NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
            self.requestCompletionHandler?(data, response, error)
            self.requestCompletionHandler = nil
            self.completeOperation()
        }
        task?.resume()
    }

    override func cancel() {
        requestCompletionHandler = nil
        super.cancel()
        task?.cancel()
    }

}

/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
///   necessary and then ensuring that `completeOperation()` is called; or
///   override `cancel` method, calling `super.cancel()` and then cleaning-up
///   and ensuring `completeOperation()` is called.

public class AsynchronousOperation : NSOperation {

    override public var asynchronous: Bool { return true }

    private let stateLock = NSLock()

    private var _executing: Bool = false
    override private(set) public var executing: Bool {
        get {
            return stateLock.withCriticalScope { _executing }
        }
        set {
            willChangeValueForKey("isExecuting")
            stateLock.withCriticalScope { _executing = newValue }
            didChangeValueForKey("isExecuting")
        }
    }

    private var _finished: Bool = false
    override private(set) public var finished: Bool {
        get {
            return stateLock.withCriticalScope { _finished }
        }
        set {
            willChangeValueForKey("isFinished")
            stateLock.withCriticalScope { _finished = newValue }
            didChangeValueForKey("isFinished")
        }
    }

    /// Complete the operation
    ///
    /// This will result in the appropriate KVN of isFinished and isExecuting

    public func completeOperation() {
        if executing {
            executing = false
            finished = true
        }
    }

    override public func start() {
        if cancelled {
            finished = true
            return
        }

        executing = true

        main()
    }
}

// this locking technique taken from "Advanced NSOperations", WWDC 2015
// https://developer.apple.com/videos/play/wwdc2015/226/

extension NSLock {
    func withCriticalScope<T>(@noescape block: Void -> T) -> T {
        lock()
        let value = block()
        unlock()
        return value
    }
}

完成此操作后,您可以启动一系列可以按顺序执行的请求:

let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 1 

for urlString in urlStrings {
    let url = NSURL(string: urlString)!
    print("queuing \(url.lastPathComponent)")
    let operation = NetworkOperation(url: url) { data, response, error in
        // do something with the `data`
    }
    queue.addOperation(operation)
}

或者,如果您不希望遭受顺序请求的显着性能损失,但仍希望限制并发度(最小化系统资源,避免超时等),则可以设置{{1等于3或4的值。

或者,您可以使用依赖项,例如在完成所有异步下载时触发​​某个进程:

maxConcurrentOperationCount

如果您想取消请求,可以轻松取消它们:

let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 3

let completionOperation = NSBlockOperation() {
    self.tableView.reloadData()
}

for urlString in urlStrings {
    let url = NSURL(string: urlString)!
    print("queuing \(url.lastPathComponent)")
    let operation = NetworkOperation(url: url) { data, response, error in
        // do something with the `data`
    }
    queue.addOperation(operation)
    completionOperation.addDependency(operation)
}

// now that they're all queued, you can queue the completion operation on the main queue, which will only start once the requests are done

NSOperationQueue.mainQueue().addOperation(completionOperation)

操作是用于控制一系列异步任务的非常丰富的机制。如果您参考WWDC 2015视频Advanced NSOperations,他们已经将这种模式与条件和观察者结合在一起(尽管他们的解决方案对于简单的问题可能有点过度设计。恕我直言。)

答案 3 :(得分:1)

以下是我在另一个simalar问题答案中建议的方法,专门针对您的问题量身定制:

您的方法method1method2都是异步的。异步函数应该有一种向调用者发出完成信号的方法。一种方法是使用完成处理程序:

    func method1(url: NSURL, completion: (Result1?, ErrorType?) -> ()) 


    func method2(url: NSURL), completion: (Result2?, ErrorType?) -> ())

这里,Result1Result2是异步函数的计算结果。由于任务可能失败,因此完成处理程序的签名具有返回计算值或错误的方法。

假设您的第一个方法method1评估了一个项目列表,每个项目都包含另一个URL。对于此列表中的每个网址,您需要致电method2

将这些组合任务包装到一个新函数method中(它也是异步的,因此它也有一个完成处理程序!):

func method(completion: (Result?, ErrorType?)-> ()) {
    let url = ...
    self.method1(url) { (result1, error) in
        if let result = result1 {
             // `result` is an array of items which have 
             // a url as property:
             let urls = result.map { $0.imageUrl } 
             // Now, for each url, call method2:
             // Use a dispatch group in order to signal
             // completion of a group of asynchronous tasks
             let group = dispatch_group_create()
             let finalResult: SomeResult?
             let finalError: ErrorType?
             urls.forEach { imageUrl in 
                 dispatch_group_enter(group)
                 self.method2(imageUrl) { (result2, error) in
                     if let result = result2 {
                     } else {
                         // handle error (maybe set finalError and break)
                     }
                     dispatch_group_leave(group)
                 }
             }
             dispatch_group_notify(dispatch_get_global_queue(0,0)) {
                 completion(finalResult, finalError)
             }
        } else {
            // Always ensure the completion handler will be
            // eventually called:
            completion(nil, error)
        }
    }
}

上述方法使用调度组来分组许多任务。任务启动时,将使用dispatch_enter增加组的任务数。完成任务后,将使用dispatch_group_leave减少组中的任务数。

当组为空(所有任务都已完成)时,将使用dispatch_group_notify提交的块在给定队列上执行。我们使用这个块来调用外部函数method的完成处理程序。

您可以在处理错误方面发挥创意。例如,您可能只想忽略第二个方法method2的失败并继续获得结果,或者您可能希望取消仍在运行的每个任务并返回错误。调用method2并编写一个&#34;结果&#34;的数组时,您也可以允许成功和失败。如finalResult,让小组成功并返回finalResult - 这会保留每次通话的详细结果。

您可能已经注意到,没有办法取消任务。是的,没有。这将需要可取消的任务。对于这个问题也有优雅的解决方案,但这超出了这个答案。

答案 4 :(得分:-2)