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

时间:2018-12-02 17:35:36

标签: android kotlin kotlinx.coroutines

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

我对协程的理解是,如果我在launch上调用Dispatchers.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
        }
    }
}

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

作为参考,我正在使用Kotlin 1.3。

2 个答案:

答案 0 :(得分:3)

  

我对协程的理解是,如果我在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
}

答案 1 :(得分:0)

协程暂停不是神奇地“解除阻止”现有阻止网络呼叫的功能。严格来说,这是 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的{{1 }}等。这些方法还需要您提供回调,因此协程可以再次帮助您保持自然的编程模型。协程库的核心已经包含了您需要的所有部分:

  1. 一个专用的弹性线程池,如果当前所有线程都被阻止(可通过bg访问),它将始终启动一个新线程
  2. Dispatchers.IO原语,它使协程可以从一个线程跳转到另一个线程,然后返回

使用这些工具,您可以轻松编写

withContext

当协程到达JSoup调用时,它将设置UI线程空闲,并在IO线程池中的线程上执行此行。解除阻塞并获得结果后,协程将跳回UI线程。