
SpotiFlyer 音乐下载器源码解析Channel 与 SupervisorJob 如何搭建跨平台并发任务池【免费下载链接】SpotiFlyerKotlin Multiplatform Music Downloader, Supports Spotify / Gaana / Youtube Music / Jio Saavn / SoundCloud.项目地址: https://gitcode.com/gh_mirrors/sp/SpotiFlyerSpotiFlyer是一款 Kotlin 多平台音乐下载器支持从 Spotify、Jio Saavn、Gaana、SoundCloud、YouTube Music 等平台解析并批量下载歌曲。它真正敢并行的底气来自一个不到 200 行的并发组件——ParallelExecutor。本文带你读懂它如何用Channel任务队列和SupervisorJob协程监督搭建出可跨 Android / Desktop / iOS / Web 复用的并发任务池并学会把这种模式搬进自己的项目。为什么要自己造一个并发任务池批量下载一个 100 首歌的歌单时最朴素的做法是来一首下 一首——太慢全部同时下载——瞬间打爆网络、内存和 CPU。成熟的解法是限流并发同一时刻最多跑 N 个任务其余排队。Kotlin 标准库没有现成的跨平台 Semaphore 线程池SpotiFlyer 用协程三件套自己实现了一个核心源码在 ParallelExecutor.kt。整个设计只有 4 个关键角色角色实现职责任务队列ChannelOperation*(RENDEZVOUS)待执行任务的生产者-消费者管道关停信号ChannelUnit(UNLIMITED)通知工作协程退出协程监督SupervisorJob某个任务崩溃不牵连整个池子结果回执CompletableDeferred把执行结果异步交还调用方解剖 Pool 的三大核心机制1️⃣ SupervisorJob让单个任务失败不炸掉整个池子private var service: Job SupervisorJob() override val coroutineContext get() context service见 ParallelExecutor.kt#L54-L55。默认情况下任何一个协程异常都会向上传播、取消整棵协程树。而SupervisorJob切断了这种连带伤害某首歌下载失败只会标记它自己失败其余 99 首照常排队执行。这就是下载器必须故障隔离的原因。2️⃣ 两条 Channel任务流与停止令牌private var killQueue ChannelUnit(Channel.UNLIMITED) private var operationQueue ChannelOperation*(Channel.RENDEZVOUS)两条队列容量策略刻意不同killQueueUNLIMITED 无界随时能塞满停止令牌绝不阻塞负责优雅关闭operationQueueRENDEZVOUS 零容量生产者send必须等到消费者receive才放行天然形成有槽位才放新任务的限流效果不会在内存里囤积任务。工作协程的循环则是用select同时监听两条通道L98-L107val operation selectOperation*? { killQueue.onReceive { null } // 收到停止令牌 operationQueue.onReceive { it } // 收到新任务 } ?: break // 拿到令牌则退出循环 operation.execute()哪个通道先就绪就处理哪个——这是协程里多路复用的教科书写法比runBlocking轮询干净得多。3️⃣ Operation CompletableDeferred任务与结果解耦每个任务被包装成一个内部类OperationL153-L168内部持有一个CompletableDeferred。调用方提交任务后并不等待执行而是await这个回执suspend fun Result executeSuspending(block: suspend () - Result): Result withContext(coroutineContext) { val operation Operation(block) operationQueue.send(operation) // ① 入队 operation.result.await() // ② 等结果 }execute()里用try/catch把异常转成completeExceptionally保证异常一定通过 Deferred 传递给调用方而不会打断工作协程的while(true)循环。动态扩缩容与复活机制setConcurrentOperationLimitL128-L151支持运行中调整并发数需要扩容就launchProcessor()拉起新工作协程需要缩容就向killQueue发对应数量的停止令牌多余的工作协程在下一轮select时自行退出——无需强杀协程。另外两个方法完善了生命周期管理close()用atomic的compareAndSet保证只执行一次随后关闭两条通道并取消SupervisorJobcloseAndReInit()/reviveIfClosed()整体重建一个全新的池子配合下面的stopAllTasks()可以在用户点击全部取消后无缝重启。它在外真实跑什么任务ParallelExecutor通过接口ParallelProcessor暴露给业务层L33-L46executeSafelyInPool会额外把结果包装成可函数式链式处理的SuspendableEventT, Throwable即要么成功值、要么异常的 Result 类型定义在 SuspendableEvent.kt。典型使用场景① 音频转码池——MediaConverter.kt 中抽象基类直接内置一个默认 4 并发的ParallelExecutor各平台的 FFmpeg 转码实现如 DesktopMediaConverter.kt#L13-L18 的executeSafelyInPool { FFmpeg... }提交进来多个文件同时转码但永不超载。② 图片解码池——Android 端用ParallelExecutor(Dispatchers.IO, 2)限制同时最多解码 2 张原尺寸图片防止内存溢出AndroidFileManager.kt#L227-L232// Will Decode Only a small set of images at a time, to avoid Out of Memory private val parallelExecutor ParallelExecutor(Dispatchers.IO, 2)③ 下载作用域——桌面端把DownloadScope ParallelExecutor(Dispatchers.IO)作为全局下载池DesktopFileManager.kt#L65iOS 端同样以ParallelExecutor(dispatcherIO)构建 DownloaderIOSActual.kt#L20。 注意一个跨平台细节整个类只依赖kotlinx-coroutines-core与kotlinx-atomicfu没有任何平台 API所以放在commonMain后所有平台直接复用——这就是 Kotlin 多平台写一次处处限流的价值。新手 takeaway4 点可复用的设计模式限流并发 有界工作协程 任务 Channel不写线程池、不用 SemaphoreN 个launch循环 select就能实现SupervisorJob是批处理任务的标配故障隔离比快速失败更重要CompletableDeferred做异步回执把提交任务和获取结果解耦调用方可以阻塞等待也可以拿 Deferred 去做别的关闭要幂等、重启要彻底atomic.compareAndSet防重复关闭closeAndReInit整体重建比逐个修复状态更不易出错。这套不到 200 行的池子撑起了 SpotiFlyer 在四大平台上的批量下载、转码与图片缓存。想深入阅读完整实现从 parallel_executor/ 目录开始即可。【免费下载链接】SpotiFlyerKotlin Multiplatform Music Downloader, Supports Spotify / Gaana / Youtube Music / Jio Saavn / SoundCloud.项目地址: https://gitcode.com/gh_mirrors/sp/SpotiFlyer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考