在CoroutineScope中下载多张图片

3

我需要下载多张图片,在所有下载完成后(在主线程之外),执行活动中的其他操作。

我目前正在使用Glide进行下载,如下所示:

ImageDownloader.kt

class ImageDownloader {
    fun downloadPack(context: Context, path: String, pack: PackModel) {
        for (image: ImageModel in pack.images) {
            Glide.with(context)
                .asBitmap()
                .load(image.imageFileUrl)
                .listener(object : RequestListener<Bitmap> {
                    override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
                        return false
                    }

                    override fun onResourceReady(bitmap: Bitmap?, model: Any?, target: Target<Bitmap>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                        saveImage(path, pack.id, image.imageFileName, bitmap!!)
                        return true
                    }
                }).submit()
        }
    }

    private fun saveImage(path: String, id: String, fileName: String, bitmap: Bitmap) {
        val dir = File(path + id)
        if (!dir.exists()) dir.mkdirs()
        val file = File(dir, fileName)
        val out = FileOutputStream(file)
        bitmap.compress(Bitmap.CompressFormat.PNG, 75, out)
        out.flush()
        out.close()
        // to check the download progress for each image in logcat
        println("done: $fileName")
    }
}

在这个活动中,我使用以下方式在CoroutineScope中调用此方法:

PackActivity.kt

class PackActivity: AppCompatActivity() {
    private lateinit var bind: ActivityPackBinding
    private lateinit var path: String
    private lateinit var pack: PackModel
    // other basic codes

    override fun onCreate(savedInstanceState: Bundle?) {
        // other basic codes
        path = "$filesDir/images_asset/"
        pack = intent.getParcelableExtra(PACK_DATA)!!

        bind.buttonDownload.setOnClickListener {
            downloadPack()
        }
    }
    
    private fun downloadPack() {
        CoroutineScope(Dispatchers.IO).launch {
            val async = async {
                ImageDownloader().downloadPack(applicationContext, path, pack)
            }
            val result = async.await()
            withContext(Dispatchers.Main) {
                result.apply {
                    println("finished")
                    // other things todo
                }
            }
        }
    }
}

PackActivity.kt中下载所有图片后,我想继续进行其他操作,但实际上,在使用println("finished")并检查logcat后,代码甚至在第一次下载开始之前就开始执行了...

一些信息:

PackModelImageModel是我的数据类,其中PackModel具有每个包的ID及其ImageModel列表,而ImageModel则具有ImageFileNameImageFileUrl。 所有数据都来自Web请求。

我希望将图像保存在文件夹data/data/AppPackageName/files/images_asset/PackID/...中,并且通过我所做的测试,我无法使用DownloadManager直接将图像定向到应用程序的内部文件夹,因此我正在使用Glide。


这是因为Glide使用不同的线程来下载图像。 - iamanbansal
1
当你有了那个suspend fun,只需从launch块中调用它并删除您的async-await,因为它完全没有作用。async { suspendFun() }.await()suspendFun()具有完全相同的语义,但引入了不必要的开销。 - Marko Topolnik
我认为使用无用的 async await 的倾向是受到 JavaScript 影响的结果。 - George Leung
1个回答

5

首先,重要的步骤是使用 suspendCancellableCoroutine 将异步的 Glide 请求改为 suspend fun。这里是实现方式:

private suspend fun downloadBitmap(
    context: Context,
    image: ImageModel
) = suspendCancellableCoroutine<Bitmap> { cont ->
    Glide.with(context)
        .asBitmap()
        .load(image.imageFileUrl)
        .listener(object : RequestListener<Bitmap> {
            override fun onLoadFailed(
                e: GlideException, model: Any?,
                target: Target<Bitmap>?, isFirstResource: Boolean
            ): Boolean {
                cont.resumeWith(Result.failure(e))
                return false
            }

            override fun onResourceReady(
                bitmap: Bitmap, model: Any?, target: Target<Bitmap>?,
                dataSource: DataSource?, isFirstResource: Boolean
            ): Boolean {
                cont.resumeWith(Result.success(bitmap))
                return false
            }
        }).submit()
}

完成这些操作后,现在您可以轻松地进行并发下载并等待所有下载的完成:

class ImageDownloader {
    suspend fun downloadPack(context: Context, path: String, pack: PackModel) {
        coroutineScope {
            for (image: ImageModel in pack.images) {
                launch {
                    val bitmap = downloadBitmap(context, image)
                    saveImage(path, pack.id, image.imageFileName, bitmap)
                }
            }
        }
    }


    private suspend fun saveImage(
        path: String, id: String, imageFileName: String, bitmap: Bitmap
    ) {
        withContext(IO) {
            // your code
        }
    }
}

仔细观察我在上面使用的调度程序:除了 saveImage,其他所有内容都使用 Main 调度程序,saveImage 是唯一一个包含实际阻塞代码的位置。

最后,要使用所有功能,您只需要这些:

private fun downloadPack() {
    GlobalScope.launch {
        ImageDownloader().downloadPack(applicationContext, path, pack)
        println("finished")
        // other things todo
    }
}

由于阻塞代码已经安全地包含在IO调度程序中,因此所有内容都在Main调度程序上。

我在上面使用GlobalScope是因为缺乏你更大的上下文知识,但这可能是个坏主意。编写CoroutineScope(IO).launch具有相同的问题,而且需要分配更多对象。

如果用户从应用程序中导航离开或者反复导航进入和离开,会引发越来越多的后台下载。请三思而行,考虑下载的情况。在上面的代码中,由于我不够了解Glide,所以没有处理suspendCancellableCoroutine内的取消操作。你应该添加一个cont.onCancellation处理程序来确保正确性。


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