С помощью Combine, как освободить подписку после сетевого запроса - PullRequest
0 голосов
/ 30 мая 2020

Если вы используете Combine для сетевых запросов с URLSession, тогда вам нужно сохранить Subscription (также известный как AnyCancellable) - в противном случае он немедленно освобождается, что отменяет сетевой запрос. Позже, когда сетевой ответ будет обработан, вы захотите освободить подписку, потому что ее сохранение будет пустой тратой памяти.

Ниже приведен код, который это делает. Это как-то неловко, а может, и неправильно. Я могу представить себе состояние гонки, при котором сетевой запрос может начинаться и завершаться в другом потоке до того, как sub будет установлено в значение, отличное от nil.

Есть ли более лучший способ сделать это?

class SomeThing {
    var subs = Set<AnyCancellable>()
    func sendNetworkRequest() {
        var request: URLRequest = ...
        var sub: AnyCancellable? = nil            
        sub = URLSession.shared.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: MyResponse.self, decoder: JSONDecoder())
            .sink(
                receiveCompletion: { completion in                
                    self.subs.remove(sub!)
                }, 
                receiveValue: { response in ... }
            }
        subs.insert(sub!)

Ответы [ 2 ]

3 голосов
/ 30 мая 2020

Ниже приведен код, который это делает. Это как-то неловко, а может, и неправильно. Я могу представить себе состояние гонки, при котором сетевой запрос может начинаться и завершаться в другом потоке до того, как sub будет установлено значение, отличное от nil.

Опасность! Swift.Set не является потокобезопасным. Если вы хотите получить доступ к Set из двух разных потоков, вам нужно сериализовать доступы, чтобы они не перекрывались.

Что возможно в целом (хотя, возможно, не с URLSession.DataTaskPublisher) заключается в том, что издатель излучает свои сигналы синхронно, прежде чем оператор sink даже вернется. Так ведут себя Just, Result.Publisher, Publishers.Sequence и другие. Таким образом, они создают проблему, которую вы описываете, без использования потоковой безопасности.

Теперь, как решить проблему? Если вы не думаете, что действительно хотите иметь возможность отменить подписку, вы можете вообще избежать создания AnyCancellable, используя Subscribers.Sink вместо оператора sink:

        URLSession.shared.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: MyResponse.self, decoder: JSONDecoder())
            .subscribe(Subscribers.Sink(
                receiveCompletion: { completion in ... },
                receiveValue: { response in ... }
            ))

Combine очистит подписку и подписчика после завершения подписки (с помощью .finished или .failure).

Но что, если вы делаете хотите иметь возможность отменить подписку ? Может быть, иногда ваш SomeThing уничтожается до того, как подписка будет завершена, и в этом случае вам не нужна подписка для завершения. Затем вы хотите создать AnyCancellable и сохранить его в свойстве экземпляра, чтобы он отменялся при уничтожении SomeThing.

В этом случае установите флаг, указывающий, что приемник выиграл гонку , и проверьте флаг перед сохранением AnyCancellable.

        var sub: AnyCancellable? = nil
        var isComplete = false
        sub = URLSession.shared.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: MyResponse.self, decoder: JSONDecoder())
            // This ensures thread safety, if the subscription is also created
            // on DispatchQueue.main.
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    isComplete = true
                    if let theSub = sub {
                        self?.subs.remove(theSub)
                    }
                }, 
                receiveValue: { response in ... }
            }
        if !isComplete {
            subs.insert(sub!)
        }
1 голос
/ 30 мая 2020

Я называю эту ситуацию одноразовым абонентом . Идея состоит в том, что, поскольку издатель задачи данных публикует только один раз, вы точно знаете, что можно безопасно уничтожить конвейер после получения вашего единственного значения и / или завершения (ошибки).

Вот техника, которую я люблю использовать. Во-первых, вот начальная часть конвейера:

let url = URL(string:"https://www.apeth.com/pep/manny.jpg")!
let pub : AnyPublisher<UIImage?,Never> =
    URLSession.shared.dataTaskPublisher(for: url)
        .map {$0.data}
        .replaceError(with: Data())
        .compactMap { UIImage(data:$0) }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()

Теперь самое интересное. Смотри внимательно:

var cancellable: AnyCancellable? // 1
cancellable = pub.sink(receiveCompletion: {_ in // 2
    cancellable?.cancel() // 3
}) { image in
    self.imageView.image = image
}

Вы видите, что я там делал? Возможно, нет, поэтому я объясню это:

  1. Сначала я объявляю переменную local AnyCancellable; по причинам, связанным с правилами синтаксиса Swift, это должно быть необязательным.

  2. Затем я создаю своего подписчика и устанавливаю свою переменную AnyCancellable для этого подписчика. Опять же, по причинам, связанным с правилами синтаксиса Swift, мой подписчик должен быть Sink.

  3. Наконец, в самом подписчике я отменяю AnyCancellable, когда получаю завершение .

Отмена на третьем шаге фактически делает два совершенно помимо вызова cancel() - вещей, связанных с управлением памятью:

  • По , ссылаясь на на cancellable внутри функции асинхронного завершения приемника, я сохраняю cancellable и весь конвейер живым достаточно долго, чтобы значение поступают от подписчика.

  • Путем отмены cancellable я разрешаю конвейеру go прекратить существование и предотвращать цикл сохранения, который может вызвать окружающие просмотрите контроллер на предмет утечки.

...