在 CoroutineScope 中下载多个图像

Download multiple images in a CoroutineScope

我需要下载多张图片,全部下载完成后(主线程外),在activity.

执行其他操作

我目前使用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")
    }
}

在 activity 中,我在 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,代码甚至在第一次下载开始之前就开始了...

一些信息:

PackModel 和 ImageModel 是我的数据 class,其中 PackModel 具有每个包的 ID 和 ImageModel 列表,后者又具有 ImageFileName 和 ImageFileUrl。所有数据均来自网络请求。

我要将图像保存在文件夹 data/data/AppPackageName/files/images_asset/PackID/ ...通过我所做的测试,我无法使用 DownloadManager 将图像定向到应用程序的这个内部文件夹,这就是我使用 Glide 的原因。

关键的第一步是使用 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 调度程序,这是唯一包含实际阻塞代码的地方。

最后,要使用一切,这就是您所需要的:

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

同样,Main 调度程序上的所有内容,因为阻塞代码已安全地纳入 IO 调度程序。

我在上面使用 GlobalScope 是因为您对更大的上下文缺乏了解,但这可能是个坏主意。写 CoroutineScope(IO).launch 有所有相同的问题,加上分配更多的对象。

如果用户离开应用程序,或者如果他们反复离开和返回,触发越来越多的后台下载,请仔细考虑您的下载会发生什么。在上面的代码中,我没有在 suspendCancellableCoroutine 内处理取消,因为我对 Glide 不是很熟悉。您应该添加一个 cont.onCancellation 处理程序才正确。