NetworkOnMainThreadException 是否对协程中的网络调用有效?

Is NetworkOnMainThreadException valid for a network call in a coroutine?

我正在 Kotlin 中为 Android 构建一个简单的演示应用程序,它使用 Jsoup 检索网页的标题。我正在使用 Dispatchers.Main 作为上下文进行网络调用。

我对协同程序的理解是,如果我在 Dispatchers.Main 上调用 launch 它会在主线程上 执行 运行,但会暂停执行以免阻塞线程。

我对android.os.NetworkOnMainThreadException的理解是因为网络操作繁重,在主线程运行时会阻塞

所以我的问题是,鉴于协程不会阻塞它所在的线程 运行,NetworkOnMainThreadException 真的有效吗?下面是一些在 Jsoup.connect(url).get():

处抛出给定异常的示例代码
class MainActivity : AppCompatActivity() {
    val job = Job()

    val mainScope = CoroutineScope(Dispatchers.Main + job)

    // called from onCreate()
    private fun printTitle() {
        mainScope.launch {
            val url ="https://kotlinlang.org"
            val document = Jsoup.connect(url).get()
            Log.d("MainActivity", document.title())
            // ... update UI with title
        }
    }
}

我知道我可以简单地 运行 使用 Dispatchers.IO 上下文并将此结果提供给 main/UI 线程,但这似乎避开了协程的一些实用程序。

作为参考,我使用的是 Kotlin 1.3。

My understanding of coroutines is that if I call launch on the Dispatchers.Main it does run on the main thread, but suspends the execution so as to not block the thread.

为了不阻塞线程而暂停执行的唯一点是标记为 suspend 的方法 - 即暂停方法。

由于Jsoup.connect(url).get()不是挂起方法,它会阻塞当前线程。当你使用 Dispatchers.Main 时,当前线程是主线程,你的网络操作直接在主线程上运行,导致 NetworkOnMainThreadException.

像您的 get() 方法这样的阻塞工作可以通过将其包装在 withContext() 中来暂停, 一种暂停方法并确保 Dispatchers.Main 在方法运行时未被阻塞。

mainScope.launch {
    val url ="https://kotlinlang.org"
    val document = withContext(Dispatchers.IO) {
        Jsoup.connect(url).get()
    }
    Log.d("MainActivity", document.title())
    // ... update UI with title
}

协程挂起不是神奇地 "unblocks" 现有阻塞网络调用的功能。它严格来说是一个 cooperative 功能,需要代码显式调用 suspendCancellableCoroutine。因为您正在使用一些预先存在的阻塞 IO API,协程会阻塞其调用线程。

要真正利用可挂起代码的强大功能,您必须使用非阻塞 IO API,它可以让您发出请求并提供 API 将在结果为 API 时调用的回调准备好。例如:

NonBlockingHttp.sendRequest("https://example.org/document",
        onSuccess = { println("Received document $it") },
        onFailure = { Log.e("Failed to fetch the document", it) }
)

有了这种API,无论您是否使用协程,都不会阻塞任何线程。然而,与阻塞 API 相比,它的用法相当笨拙和混乱。这就是协程可以帮助您的:它们允许您以完全相同的形式继续编写代码,就好像它是阻塞的,只是它不是。要获得它,您必须首先编写一个 suspend fun 将您拥有的 API 转换为协程挂起:

suspend fun fetchDocument(url: String): String = suspendCancellableCoroutine { cont ->
    NonBlockingHttp.sendRequest(url,
            onSuccess = { cont.resume(it) },
            onFailure = { cont.resumeWithException(it) }
    )
}

现在你的调用代码回到这个:

try {
    val document = fetchDocument("https://example.org/document")
    println("Received document $document")
} catch (e: Exception) {
    Log.e("Failed to fetch the document", e)
}

相反,如果您可以保留阻塞网络 IO,这意味着每个并发网络调用都需要一个专用线程,那么如果没有协程,您将不得不使用异步任务之类的东西,Anko 的 bg 等。这些方法还要求您提供回调,因此协程可以再次帮助您保持自然的编程模型。核心协程库已经包含了你需要的所有部分:

  1. 一个专门的弹性线程池,如果当前所有线程都被阻塞,它总是启动一个新线程(可通过 Dispatchers.IO 访问)
  2. withContext 原语,它允许您的协程从一个线程跳转到另一个线程然后返回

使用这些工具,您可以简单地编写

try {
    val document = withContext(Dispatchers.IO) {
        JSoup.connect("https://example.org/document").get()
    }
    println("Received document $it")
} catch (e: Exception) {
    Log.e("Failed to fetch the document")
}

当您的协程到达 JSoup 调用时,它将释放 UI 线程并在 IO 线程池中的线程上执行此行。当它解除阻塞并得到结果时,协程将跳回到 UI 线程。