【问题标题】:Parallel URLSession requests w/ DispatchGroup call completion handler twice on 1 request并行 URLSession 请求 w/ DispatchGroup 在 1 个请求上调用完成处理程序两次
【发布时间】:2019-11-27 12:24:53
【问题描述】:

使用DispatchGroup 我正在尝试对我的客户端运行 2 个网络请求,并在两者都完成后返回结果。

我有一个问题,有时DispatchGroup 请求之一的完成处理程序被调用两次,而另一个根本没有被调用。

一个例子是 -


    func fetchProfileWithRelatedArticle(onSuccess: @escaping (User, [RelatedArticle]) -> Void, onError: @escaping (Error) -> Void) {
        let dispatchGroup = DispatchGroup()

        var user: User?
        var articles: [RelatedArticle] = []

        var errors: [Error] = []

        dispatchGroup.enter()
        fetchProfileForUser(onSuccess: {
            user = $0
            print("fetchProfile:",$0)
            print("123")
            dispatchGroup.leave()
        }, onError: { error in
            errors.append(error)
            dispatchGroup.leave()
        })

        dispatchGroup.enter()
        getArticlesForUser(onSuccess: {
            articles = $0
            print("getArticlesForUser:",$0)
            print("456")
            dispatchGroup.leave()
        }, onError: { error in
            errors.append(error)
            dispatchGroup.leave()
        })

        dispatchGroup.notify(queue: .main) {
            guard let user = user, errors.isEmpty else { return }

            onSuccess(user, articles)
        }
    }

在这里,我获取用户个人资料并获取他们撰写的文章列表。这些通过完成处理程序返回并在其他地方呈现。

大多数情况下这有效,但有时这些请求中的任何一个都会调用自己的完成处理程序两次,而另一个请求不会。

我怀疑这可能是因为我的访问令牌过期了,因为如果我离开应用程序一小段时间就会发生这种情况。我的访问令牌的有效期为 2 分钟。

如果请求收到401 响应,我的网络客户端中有以下方法请求新令牌,然后再次调用该调用。我相信这可能无法如我所愿。


            if response.statusIs401() {
                self?.refreshHandler { success in
                    guard success else { completion(.failure(TokenError.refused)); return }
                    self?.request(resource, completion)
                }
                return
            }

我怀疑在更新后再次调用该方法是对我的调度组返回的请求执行某些操作。

是否可以以这种方式链接请求?


struct NoContent: Codable { }
typealias RefreshHandler = (@escaping (Bool) -> Void) -> ()
typealias TokenGetter = () -> [String: String]

protocol ClientType: class {
    associatedtype Route: RouterType
    func request<T: Codable>(_ resource: Route, _ completion: @escaping  (Result<T>)-> Void)
}

class Client<Route: RouterType>: ClientType {

    enum APIError: Error {
        case unknown, badResponse, jsonDecoder, other
    }

    enum TokenError: String, Error {
        case expired = "Access Token Expired"
        case refused = "Refresh Token Failed"
    }

    private(set) var session: SessionType
    private(set) var tokenGetter: TokenGetter
    private(set) var refreshHandler: RefreshHandler

    private lazy var decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
        return decoder
    }()

    init(session: SessionType, tokenGetter: @escaping TokenGetter, refreshHandler: @escaping RefreshHandler) {
        self.session = session
        self.tokenGetter = tokenGetter
        self.refreshHandler = refreshHandler
    }

    func request<T: Codable>(_ resource: Route, _ completion: @escaping  (Result<T>)-> Void) {

        let request = URLRequest(
            resource: resource,
            headers: tokenGetter()
        )

        URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
            guard error == nil else { completion(.failure(APIError.unknown)); return }
            guard let response = response as? HTTPURLResponse else { completion(.failure(APIError.badResponse)); return }

            if response.statusIs401() {
                self?.refreshHandler { success in
                    guard success else { completion(.failure(TokenError.refused)); return }
                    self?.request(resource, completion)
                }
                return
            }

            if response.statusIsSuccess() {
                guard let self = self, let data = self.deserializeNoContentResponse(data: data) else { completion(.failure(APIError.badResponse)); return }
                do {
                    let value = try self.decoder.decode(T.self, from: data)
                    DispatchQueue.main.async {
                        completion(.success(value))
                    }
                } catch let error {
                    print(error)
                }
                return
            }

            completion(.failure(APIError.other))
        }.resume()
    }

    // some calls return a 200/201 with no data
    private func deserializeNoContentResponse(data: Data?) -> Data? {

        if data?.count == 0 {
            return "{ }".data(using: .utf8)
        }

        return data
    }
}

【问题讨论】:

    标签: swift grand-central-dispatch urlsession


    【解决方案1】:

    听起来您需要在网络客户端中执行以下操作:

    func makeTheRequest(_ completion: CompletionHandler) {
      URLSession.shared.dataTask(with: someURL) { data, response, error in 
        guard let httpResponse = response as? HTTPURLResponse else {
          return
        }
    
        if httpResponse.statusCode == 401 {
          self.refreshToken { success in
            if success { self.makeTheRequest(completion) }
          }
        }
    
        // handle response
        completion(whateverDataYouNeedToPass)
      }
    }
    

    这将进行调用,检查响应代码,需要刷新令牌,如果成功,它会调用应该再次使用完成处理程序再次做出响应的方法,该完成处理程序首先传递给它,而不是先调用它。因此,直到第二次调用 API 之后才会调用完成处理程序。

    当然,在你自己的代码中采用这个,应该不会太难做

    【讨论】:

    • 我的response.statusIs401() if 语句中已经有类似的东西,但它再次调用`func request`,而不是其他方法。你认为这有什么不同还是应该用原始的完成处理程序调用自己?我有点不确定你的建议会有什么不同?感谢您的帮助,非常感谢。
    • 再次调用该方法就可以了,这正是您在这种情况下想要的。如果响应是 401,请刷新令牌并重试 :) 是的,这与您已经做过的几乎相同。
    猜你喜欢
    • 2021-10-26
    • 1970-01-01
    • 2019-11-16
    • 1970-01-01
    • 2015-12-18
    • 1970-01-01
    • 1970-01-01
    • 2016-10-03
    • 1970-01-01
    相关资源
    最近更新 更多