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

Dan*_*n 0 12 android kotlin kotlinx.coroutines

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

我的协同程序的理解是,如果我叫launchDispatchers.Main在主线程运行,但暂停执行,从而不会阻塞线程。

我的理解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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

ian*_*ake 11

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

挂起执行以便不阻塞线程的唯一点是标记为的方法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
}
Run Code Online (Sandbox Code Playgroud)

  • 您应该会看到一条 Lint 警告,上面写着“冗余‘挂起’修饰符:如果内部没有调用其他挂起函数,此检查报告挂起修饰符是多余的。” - 仅仅因为您将某些内容标记为挂起并不意味着它实际上会挂起 - 这就是 `withContext` 的用途。 (2认同)

Mar*_*nik 5

协程挂起并不是一个可以神奇地“解锁”现有阻塞网络调用的功能。它严格来说是一个协作功能,需要代码显式调用suspendCancellableCoroutine. 因为您正在使用一些预先存在的阻塞 IO API,所以协程会阻塞其调用线程。

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

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

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

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

现在你的调用代码又回到了这里:

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

相反,如果您愿意保留阻塞网络 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")
}
Run Code Online (Sandbox Code Playgroud)

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