使用OkHttp和协程在Android上下载多个文件

4

在我的应用程序中,我从一个API获取一组图片的URL并需要使用这些URL创建 Bitmap 对象,以便能够在UI中显示图像。我看到Android文档建议使用协程来执行此类异步任务,但我不确定如何正确地实现它。

使用OkHttp作为我的HTTP客户端,我尝试了以下方法:

<code>GlobalScope.launch {
                    val gson = Gson();
                    val parsedRes = gson.fromJson(
                        response.body?.charStream(),
                        Array<GoodreadsBook>::class.java
                    );
                    // Create the bitmap from the imageUrl
                    for (i in 0 until parsedRes.size) {
                        val bitmap =
                            GlobalScope.async { createBitmapFromUrl(parsedRes[i].best_book.image_url) }
                        parsedRes[i].best_book.imageBitmap = bitmap.await();
                    }
                   searchResults.postValue(parsedRes)
                }
</code>

response 是我从 API 返回的内容,searchResults 是一个包含解析后响应的 LiveData。 同时,以下是我如何从这些 URL 获取图像:

suspend fun createBitmapFromUrl(url: String): Bitmap? {
    val client = OkHttpClient();
    val req = Request.Builder().url(url).build();
    val res = client.newCall(req).execute();
    return BitmapFactory.decodeStream(res.body?.byteStream())
}

尽管每个获取操作都在单独的协程中执行,但速度仍然太慢。有更好的方法吗?如果有针对协程进行优化的其他http客户端,我可以使用它,但我是Kotlin新手,不知道有哪些。


2
首先,您应该确定缓慢的原因,这可能完全超出了此代码片段的范围。是服务器吗?您的互联网连接? - Thomas
1
bitmap.await() 你在循环中使用了await,这会暂停循环直到该deferred完成。 - Animesh Sahu
1
考虑使用 Picasso、Glide 或 Coil,它们将为您下载并缓存图片。 - Fabio
@Fabio,这两者之间有性能差异吗? - Adrian Pascu
没关系,我又仔细阅读了数据绑定文档,找到了如何做的方法。这可能是最好的方法,谢谢! - Adrian Pascu
显示剩余5条评论
2个回答

4

首先createBitmapFromUrl(url: String) 在同步模式下运行,你需要先阻止它在协程线程中的阻塞,你可能想使用 Dispatchers.IO 来完成这个任务,因为回调并不是异步协程中最常用的方式。

val client = OkHttpClient()  // preinitialize the client

suspend fun createBitmapFromUrl(url: String): Bitmap? = withContext(Dispatchers.IO) {
    val req = Request.Builder().url(url).build()
    val res = client.newCall(req).execute()
    BitmapFactory.decodeStream(res.body?.byteStream())
}

现在,当你调用bitmap.await()时,你实际上是在说:“嘿,等待deferred的bitmap完成,一旦它完成了,恢复循环进行下一次迭代”

因此,你可能想在协程内部进行赋值,以防止它挂起循环,否则创建另一个循环来处理。我会选择第一种选项。

scope.launch {
    val gson = Gson();
    val parsedRes = gson.fromJson(
        response.body?.charStream(),
        Array<GoodreadsBook>::class.java
    );
    // Create the bitmap from the imageUrl
    for (i in 0 until parsedRes.size) {
        launch {
            parsedRes[i].best_book.imageBitmap = createBitmapFromUrl(parsedRes[i].best_book.image_url)
        }
    }
}

我无法从 withContext 块中返回一个值。 - Adrian Pascu
@AdrianPascu 对不起,只需删除 return 关键字,lambda 中的最后一条语句会自动返回 :) 已修复。否则,您可以使用限定返回,例如 return@withContext <返回值> - Animesh Sahu
哦,没错,那是一个 Lambda。我还不太习惯这种块语法。我会试一下的。 - Adrian Pascu
没错,这个可行。谢谢!不过网络请求还是有点慢,我猜这可能是由托管那些图片的服务器所限制的。从后台服务运行获取作业是否能够在一定程度上弥补网络减速呢? - Adrian Pascu
我得试试那个。有很多Kotlin特性我不知道...非常感谢! - Adrian Pascu
显示剩余4条评论

0
使用以下类似的库,它不使用阻塞的execute方法,而是从异步的enqueue桥接。

https://github.com/gildor/kotlin-coroutines-okhttp

suspend fun main() {
    // Do call and await() for result from any suspend function
    val result = client.newCall(request).await()
    println("${result.code()}: ${result.message()}")
}

这个基本上做的是以下的事情

public suspend fun Call.await(): Response {
    return suspendCancellableCoroutine { continuation ->
        enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                continuation.resume(response)
            }

            override fun onFailure(call: Call, e: IOException) {
                if (continuation.isCancelled) return
                continuation.resumeWithException(e)
            }
        })

        continuation.invokeOnCancellation {
            try {
                cancel()
            } catch (ex: Throwable) {
                //Ignore cancel exception
            }
        }
    }
}

这似乎在分析器中看起来更快,但差异并不明显。 - Adrian Pascu
当然可以。但使用线程更有效率。执行解决方案没有问题。 - Yuri Schimke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接