处置(取消)可观察到的。 SubscribeOn和observeOn不同的调度程序

时间:2018-08-15 10:21:59

标签: ios swift rx-swift

已修正的问题

我已经改过了我的问题。对于普通情况。

我想在后台线程中使用RxSwift生成项目(从磁盘加载,长时间运行的计算等),并观察MainThread中的项目。而且我想确保在处理后(从主线程)不会交付任何项目。

根据文档(https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#disposing):

  

那么执行dispose调用后,此代码可以打印某些内容吗?答案是:这取决于。

     
      
  • 如果调度程序是一个串行调度程序(例如MainScheduler),并且在同一串行调度程序上调用了dispose,则答案为否。

  •   
  • 否则,是的。

  •   

但是如果在不同的调度程序中使用subscribeOn和observer-我们不能保证在处理后不会发出任何东西(手动或通过处理袋,这没关系)。

我应该如何在后台生成项目(例如图像),并确保处理后不会使用结果?

我在实际项目中进行了变通,但我想解决此问题并了解在相同情况下应如何避免。

在我的测试项目中,我花了很少的时间-他们完美地演示了这个问题!

import RxSwift

class TestClass {
    private var disposeBag = DisposeBag()

    private var isCancelled = false

    init(cancelAfter: TimeInterval, longRunningTaskDuration: TimeInterval) {
        assert(Thread.isMainThread)

        load(longRunningTaskDuration: longRunningTaskDuration)

        DispatchQueue.main.asyncAfter(deadline: .now() + cancelAfter) { [weak self] in
            self?.cancel()
        }
    }

    private func load(longRunningTaskDuration: TimeInterval) {
        assert(Thread.isMainThread)

        // We set task not cancelled
        isCancelled = false

        DataService
            .shared
            .longRunngingTaskEmulation(sleepFor: longRunningTaskDuration)
            // We want long running task to be executed in background thread
            .subscribeOn(ConcurrentDispatchQueueScheduler.init(queue: .global()))
            // We want to process result in Main thread
            .observeOn(MainScheduler.instance)
            .subscribe(onSuccess: { [weak self] (result) in
                assert(Thread.isMainThread)

                guard let strongSelf = self else {
                    return
                }

                if !strongSelf.isCancelled {
                    print("Should not be called! Task is cancelled!")
                } else {
                    // Do something with result, set image to UIImageView, for instance
                    // But if task was cancelled, this method will set invalid (old) data
                    print(result)
                }

                }, onError: nil)
            .disposed(by: disposeBag)
    }

    // Cancel all tasks. Can be called in PreapreForReuse.
    private func cancel() {
        assert(Thread.isMainThread)

        // For test purposes. After cancel, old task should not make any changes.
        isCancelled = true

        // Cancel all tasks by creating new DisposeBag (and disposing old)
        disposeBag = DisposeBag()
    }
}

class DataService {
    static let shared = DataService()

    private init() { }

    func longRunngingTaskEmulation(sleepFor: TimeInterval) -> Single<String> {
        return Single
            .deferred {
                assert(!Thread.isMainThread)

                // Enulate long running task
                Thread.sleep(forTimeInterval: sleepFor)

                // Return dummy result for test purposes.
                return .just("Success")
        }
    }
}

class MainClass {
    static let shared = MainClass()

    private init() { }

    func main() {

        Timer.scheduledTimer(withTimeInterval: 0.150, repeats: true) { [weak self] (_) in
            assert(Thread.isMainThread)

            let longRunningTaskDuration: TimeInterval = 0.050

            let offset = TimeInterval(arc4random_uniform(20)) / 1000.0
            let cancelAfter = 0.040 + offset

            self?.executeTest(cancelAfter: cancelAfter, longRunningTaskDuration: longRunningTaskDuration)
        }
    }

    var items: [TestClass] = []
    func executeTest(cancelAfter: TimeInterval, longRunningTaskDuration: TimeInterval) {
        let item = TestClass(cancelAfter: cancelAfter, longRunningTaskDuration: longRunningTaskDuration)
        items.append(item)
    }
}

在某个地方调用MainClass.shared.main()开始。

我们调用方法来加载一些数据,然后我们调用cancel(全部来自主线程)。取消后,有时我们也会在主线程中收到结果,但是它已经很旧了。

在实际项目中,TestClass是UITableViewCell子类,在prepareForReuse中调用cancel方法。然后,单元将被重用,并将新数据设置到该单元。然后我们得到OLD任务的结果。并将旧图像设置为该单元格!


原始问题(旧):

我想在iOS中使用RxSwift加载图像。我想在后台加载图像,并在主线程中使用它。所以我订阅了后台线程,并观察了主线程。函数将如下所示:

func getImage(path: String) -> Single<UIImage> {
    return Single
        .deferred {
            if let image = UIImage(contentsOfFile: path) {
                return Single.just(image)
            } else {
                return Single.error(SimpleError())
            }
        }
        .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
        .observeOn(MainScheduler.instance)
}

但是我遇到了取消问题。由于使用不同的调度程序来创建项目和调用处置(从主线程进行处置),因此可以在调用处置之后引发订阅事件。因此,在UITableViewCell中使用的情况下,我会收到无效的(旧)图像。

如果我在观察(主线程)的同一调度程序中创建项目(加载图像),则一切正常! 但是我想在后台加载图像,并且希望在处理后将其取消(在prepareForReuse方法或新的路径设置方法中)。常见的模板是什么?

编辑:

我创建了一个测试项目,可以在处置后收到事件时模拟该问题。

我有一个可行的简单解决方案。我们应该在同一调度程序中发出项目。因此,我们应该捕获调度程序并在那里发送项目(在长时间运行的任务完成之后)。

func getImage2(path: String) -> Single<UIImage> {
    return Single
        .create(subscribe: { (single) -> Disposable in
            // We captrure current queue to execute callback in
            // TODO: It can be nil if called from background thread
            let callbackQueue = OperationQueue.current

            // For async calculations
            OperationQueue().addOperation {
                // Perform any long-running task
                let image = UIImage(contentsOfFile: path)

                // Emit item in captured queue
                callbackQueue?.addOperation {
                    if let result = image {
                        single(.success(result))
                    } else {
                        single(.error(SimpleError()))
                    }
                }
            }

            return Disposables.create()
        })
        .observeOn(MainScheduler.instance)
}

但这不是Rx方式。而且我认为这不是最佳解决方案。

可能是我应该使用CurrentThreadScheduler发出项目,但我不知道如何。是否有使用调度程序生成项目的任何教程或示例?我没找到。

1 个答案:

答案 0 :(得分:0)

有趣的测试用例。有一个小错误,应该是if strongSelf.isCancelled而不是if !strongSelf.isCancelled。除此之外,测试用例还显示了问题。

我可以直观地期望,如果在同一线程上进行处理,则在发出之前检查是否已经进行了处理。

我另外发现了这一点:

  

只是为了澄清这一点,如果您在一个线程上调用dispose(例如   main),则不会在同一线程上观察到任何元素。这是一个   保证。

请参阅此处:https://github.com/ReactiveX/RxSwift/issues/38

所以也许是一个错误。

请确保我在这里打开了一个问题: https://github.com/ReactiveX/RxSwift/issues/1778

更新

似乎实际上是一个错误。同时,RxSwift的优秀人员已经确认了这一点,幸运的是,修复得非常快。请参阅上面的问题链接。

测试

该错误已通过提交bac86346087c7e267dd5a620eed90a7849fd54ff进行了修复。因此,如果您使用的是CocoaPods,则可以简单地使用以下内容进行测试:

target 'RxSelfContained' do
  use_frameworks!
  pod 'RxAtomic', :git => 'https://github.com/ReactiveX/RxSwift.git', :commit => 'bac86346087c7e267dd5a620eed90a7849fd54ff'
  pod 'RxSwift', :git => 'https://github.com/ReactiveX/RxSwift.git', :commit => 'bac86346087c7e267dd5a620eed90a7849fd54ff'
end