【问题标题】:Why should I subscribe on the main thread?为什么要订阅主线程?
【发布时间】:2018-09-22 10:11:45
【问题描述】:

试图从这个资源中找出一些东西:https://www.raywenderlich.com/384-reactive-programming-with-rxandroid-in-kotlin-an-introduction

我遇到了一个问题:为什么我应该在主线程中调用 subscribeOn() 而不是 Schedulers.io()?

当我进行这样的订阅时,我的应用会在几秒钟后卡住,并且我正在丢帧。

searchTextObservable
                .subscribeOn(Schedulers.io())
                .map { cheeseSearchEngine.search(it) }
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe {
                    showResult(it)
                }

然后我在主线程中订阅并在 Schedulers.io() 中观察它(我也不明白我为什么要这样做)应用程序根本没有冻结。

searchTextObservable
                .subscribeOn(AndroidSchedulers.mainThread())
                .observeOn(Schedulers.io())
                .map { cheeseSearchEngine.search(it) }
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe {
                    showResult(it)
                }

谁能解释一下为什么会这样?

编辑

// 1
private fun createTextChangeObservable(): Observable<String> {
  // 2
  val textChangeObservable = Observable.create<String> { emitter ->
    // 3
    val textWatcher = object : TextWatcher {

      override fun afterTextChanged(s: Editable?) = Unit

      override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

      // 4
      override fun onTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        s?.toString()?.let { emitter.onNext(it) }
      }

    }

    // 5
    queryEditText.addTextChangedListener(textWatcher)

    // 6
    emitter.setCancellable {
      queryEditText.removeTextChangedListener(textWatcher)
    }
  }

  // 7
  return textChangeObservable
}

【问题讨论】:

  • 你不需要在主线程上做长时间的计算!当你想用 UI 做一些事情时,你应该只使用AndroidSchedulers.mainThread()!否则你总是可以使用Schedulers.io()
  • 能否提供searchTextObservable的代码?
  • @HansWurst,我已经提供了。请在编辑下找到它。

标签: android rx-java


【解决方案1】:

subscribeOn 与 observeOn

subscribeOn 会在给定的调度器上调用 Observable 的 create 方法。使用 subscribeOn 多少次都没有关系。第一个 subscribeOn 到 source-observable(链中的第一个)总是获胜。

observeOn 将线程从操作员切换到操作员。当上游在线程 X 上发出一个值时,它将从 observeOn-Operator 中的给定调度程序切换到线程 Y。现在,observeOn 下面的所有内容都将在 Thread Y 中处理。

对提供的示例 1 的最佳猜测: 使用 subscribeOn 将在 Schedulers#io 上调用 Observable#create。 create-lambda 中的所有内容都将在此线程上从 Schedulers#io 调用。侦听器回调 (onTextChanged) 实际上可以发生在另一个线程上。在这种情况下,它是 UI-Thread,因为它是某种 UI 元素。现在 onNext 将从 UI-Thread (emitter.onNext(it)) 调用。该值将被发送到 UI 线程 (.map { cheeseSearchEngine.search(it) }) 上的 #map 运算符,并且 cheeseSearchEngine#search 将阻塞 UI 线程。

示例 2: 用作第一个运算符“.subscribeOn(AndroidSchedulers.mainThread())”。这实际上没有任何效果,因为您已经在 UI-Thread 中。在这种情况下,将从 AndroidSchedulers#mainThread 调用 create-lambda。 onNext 也将在 UI-Thread 上发出,就像在 Example1 中一样,因为 UI 会触发 onTextChanged-Event。然后将该值放入observeOn(Schedulers.io())。来自 observeOn-point 的所有内容都将在 Schedulers#io-Thread 上执行。当 map 执行一些 HTTP 请求(或某种长时间运行的 IO)时,这反过来不会阻塞 ui。在 map 完成并向下游发出下一个值后,下一个 observeOn(AndroidSchedulers.mainThread()) 将切换回 UI 线程。因此,您现在可以在 subscribe-lambda 中安全地更改 UI,因为您在 UI-Thread 上。 作为结论,Example2 中的第一个 subscribeOn 可以省略,如果侦听器注册发生在哪个线程无关紧要(listener-reg 可能必须是线程安全的)。

总结: 使用 subscribeOn 只会在给定的调度程序线程上调用 create lambda。 create 中已注册侦听器的回调可能发生在另一个线程上。 这就是为什么 Example1 会阻塞 UI 线程而 Example2 不会。

【讨论】:

  • 感谢您的解释!
【解决方案2】:

这就是 Rx 的美妙之处。轻松切换线程。基本上在 Rx 中,我们可以通过调用 subscribeOn() 或 ObserveOn() 在不同的线程之间切换。这两者之间的区别在于,当调用 subscribeOn(Thread1) 时,任务(在您的示例中 - cheeseSearchEngine.search(it))在 Thread1 上运行。

但是,当您调用 observeOn(Thread2) 时,执行任务的结果将交给 Thread2。这意味着结果将在 Thread2 上工作。 (在您的示例中,将在 Thread2 上调用 showResult)

所以当你调用 subscribeOn(Schedulers.io()) 时,任务是在 IO 线程上完成的。一旦结果准备好,它将在调用 observeOn(AndroidSchedulers.mainThread()) 时提供给主 UI 线程。

反之亦然,您基本上是在尝试在 UI 线程上执行任务,而不是使用 IO 后台线程。使用这种方法,如果您尝试更新任何 UI 元素,则会引发异常,提示“无法从后台线程访问 UI 元素(CalledFromWrongThreadException:只有创建视图层次结构的原始线程才能触摸其视图)”。

希望我能回答你的问题。在 Rx 中快乐编码。

【讨论】:

  • 感谢您的回答,但我还是不明白。据我了解 - 我应该调用 subscribeOn() 来使用 IO 线程进行所有计算。然后使用 observeOn(UI thread) 在 UI 线程上表示结果。但是在这个例子中,如果我调用 subscribeOn(Schedulers.IO) 来进行计算,我会丢帧。我不明白为什么会这样。我是否清楚,我应该调用 subscribeOn(io) 进行计算并使用 observeOn(UI) 表示结果?
  • 嗯,也许答案是 - 当您使用 UI 元素(而不是服务器调用)时 - 您应该订阅 UI 线程,而使用服务器调用订阅相反的线程?
  • “首先,指定链下的代码应该从主线程而不是 I/O 线程开始。在 Android 中,所有与视图一起使用的代码都应该在主线程上执行。” - 这是我从教程中得到的。
【解决方案3】:

我认为代码有点误导,因为 .map 运算符实际上用于执行昂贵的操作(搜索)。更好的方法是使用 fromCallable 包装代码,然后使用 subscribeOn 将其转换为异步调用。类似的东西:

searchTextObservable
            // Means start a new async search every time text is changed
            .flatMapSingle { Single
                .fromCallable { cheeseSearchEngine.search(it) }
                // This makes sure search is running on IO thread
                // This way expensive operation is done off the main thread, which eliminates the freeze
                .subscribeOn(Schedulers.io()) }
            // This makes sure that results will be handled on main thread
            // Important because you can only access Android Widgets from the main thread
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe {
                showResult(it)
            }

解释在代码中。我认为现在的意图要清楚得多。有关详细信息,请参阅 HansWursrt 的答案

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-22
    • 1970-01-01
    • 2021-05-03
    相关资源
    最近更新 更多