Swift 实现:三级下载队列

1. 自定义的硬队列

下载逻辑的三级队列,第一级取3个、第二季取2个、第三级取1个并追加至正在执行的队列中,依此循环,异步下载方法从正在执行的队列的头部读取任务,依次执行。

代码如下:

import Foundation
import Combine
import Moya

extension WorkPreload {
    
    class PreloadItem {
        
        var localPath: String?
        var isCompleted = false
        // var progress: Float = 0.0
        
        let url: URL
        let taskId: String
        let priority: PreloadPriority
        
        init(url: URL, taskId: String, priority: PreloadPriority) {
            self.url = url
            self.taskId = taskId
            self.priority = priority
        }
        
    }
    
}

class WorkPreload {
    
    enum PreloadPriority: Int {
        case high = 1
        case medium = 2
        case low = 3
    }
    
    static let shared = WorkPreload.init(concurrentCount: 3)
    
    @ThreadSafetyWrapper private var highList: [PreloadItem] = []
    @ThreadSafetyWrapper private var mediumList: [PreloadItem] = []
    @ThreadSafetyWrapper private var lowList: [PreloadItem] = []
    
    @ThreadSafetyWrapper private var activeDownloads: [PreloadItem] = []
    @ThreadSafetyWrapper private var concurrentCount: Int = 3
    
    @ThreadSafetyWrapper private var cyclePosition = 0
    @ThreadSafetyWrapper private var paused = false
    
    @ThreadSafetyWrapper private var cancellables: [String: AnyCancellable] = [:]
    
    init(concurrentCount: Int) {
        self.concurrentCount = concurrentCount
    }
    
    // MARK: - Add DownloadItem
    
    private func addDownloadItem(url: URL, taskId: String, priority: PreloadPriority = .medium) {
        HXLogger.info("[VideoDownloadService] [addDownloadItem] >> url: \(url), taskId: \(taskId), priority: \(priority)")
        guard !isAlreadyExist(url: url) else { return }
        
        let item = PreloadItem(url: url, taskId: taskId, priority: priority)
        switch priority {
        case .high:
            highList.append(item)
        case .medium:
            mediumList.append(item)
        case .low:
            lowList.append(item)
        }
        processNextItem()
    }
    
    private func processNextItem() {
        guard !paused, activeDownloads.count < concurrentCount else { return }
        var itemsToProcess = [PreloadItem]()
        
        switch cyclePosition {
        case 0:
            itemsToProcess = takeFromList(&highList, count: 3)
            cyclePosition = 1
        case 1:
            itemsToProcess = takeFromList(&mediumList, count: 2)
            cyclePosition = 2
        case 2:
            itemsToProcess = takeFromList(&lowList, count: 1)
            cyclePosition = 0
        default:
            break
        }
        itemsToProcess.forEach {
            executeDownload($0)
        }
    }
    
    private func takeFromList(_ list: inout [PreloadItem], count: Int) -> [PreloadItem] {
        let items = Array(list.prefix(count))
        list.removeFirst(min(count, list.count))
        return items
    }
    
    private func isAlreadyExist(url: URL) -> Bool {
        return highList.contains(where: { $0.url == url }) ||
               mediumList.contains(where: { $0.url == url }) ||
               lowList.contains(where: { $0.url == url }) ||
               activeDownloads.contains(where: { $0.url == url })
    }
    
    // MARK: - ExecuteDownload
    
    private func executeDownload(_ item: PreloadItem) {
        let cancellable = AssetLoader.shared
            .publisher(.videoURL(item.url))
            .map({ $0.path })
            .sink(receiveCompletion: { [unowned self] com in
                if case .failure = com {
                    downloadFailed(item)
                } else {
                    downloadCompleted(item)
                }
            }, receiveValue: { [unowned self] in
                item.localPath = $0
                updateTask(item)
            })
        cancellables[item.taskId] = cancellable
        activeDownloads.append(item)
    }
    
    private func downloadCompleted(_ item: PreloadItem) {
        item.isCompleted = true
        activeDownloads.removeAll { $0 === item }
        cancellables.removeValue(forKey: item.taskId)
        processNextItem()
    }
    
    private func downloadFailed(_ item: PreloadItem) {
        activeDownloads.removeAll { $0 === item }
        cancellables.removeValue(forKey: item.taskId)
        processNextItem()
    }
    
    private func updateTask(_ item: PreloadItem) {
        guard let localPath = item.localPath else {
            return
        }
        guard let task = TaskManager.shared.allTaskList.first(where: { $0.taskId == item.taskId }) else {
            return
        }
        Logger.info("[VideoDownloadService] [updateTask] >> localPath: \(localPath)")
        task.workVideoLocalPath = localPath
        task.save()
    }
    
    // MARK: - Actions
    
    func pauseSerivce() {
        paused = true
    }
    
    func resumeSerivce() {
        paused = false
        processNextItem()
    }
    
    func cancelDownload(taskId: String) {
        highList.removeAll { $0.taskId == taskId }
        mediumList.removeAll { $0.taskId == taskId }
        lowList.removeAll { $0.taskId == taskId }
        activeDownloads.removeAll { $0.taskId == taskId }
        let cancellable = cancellables[taskId]
        cancellables.removeValue(forKey: taskId)
        cancellable?.cancel()
    }
    
}

2. 基于Operation实现的三级队列

import Foundation

// 定义下载操作
class DownloadOperation: Operation {
    let url: URL

    init(url: URL) {
        self.url = url
    }

    override func main() {
        if isCancelled {
            return
        }

        // 模拟下载任务
        downloadFile(from: url)
    }

    private func downloadFile(from url: URL) {
        print("Downloading from \(url)")
        // 模拟下载时间
        sleep(2)
        print("Finished downloading from \(url)")
    }
}

// 创建下载队列管理器
class DownloadQueueManager {
    static let shared = DownloadQueueManager()

    private let highPriorityQueue: OperationQueue
    private let mediumPriorityQueue: OperationQueue
    private let lowPriorityQueue: OperationQueue

    private init() {
        highPriorityQueue = OperationQueue()
        mediumPriorityQueue = OperationQueue()
        lowPriorityQueue = OperationQueue()

        highPriorityQueue.qualityOfService = .userInitiated
        mediumPriorityQueue.qualityOfService = .utility
        lowPriorityQueue.qualityOfService = .background
    }

    func addDownload(url: URL, priority: DownloadPriority) {
        let operation = DownloadOperation(url: url)
        switch priority {
        case .high:
            highPriorityQueue.addOperation(operation)
        case .medium:
            mediumPriorityQueue.addOperation(operation)
        case .low:
            lowPriorityQueue.addOperation(operation)
        }
    }
}

enum DownloadPriority {
    case high
    case medium
    case low
}

// 示例使用
let urls = [
    URL(string: "https://example.com/high1")!,
    URL(string: "https://example.com/medium1")!,
    URL(string: "https://example.com/low1")!,
    URL(string: "https://example.com/high2")!,
    URL(string: "https://example.com/medium2")!,
    URL(string: "https://example.com/low2")!
]

DownloadQueueManager.shared.addDownload(url: urls[0], priority: .high)
DownloadQueueManager.shared.addDownload(url: urls[1], priority: .medium)
DownloadQueueManager.shared.addDownload(url: urls[2], priority: .low)
DownloadQueueManager.shared.addDownload(url: urls[3], priority: .high)
DownloadQueueManager.shared.addDownload(url: urls[4], priority: .medium)
DownloadQueueManager.shared.addDownload(url: urls[5], priority: .low)

// 保持程序运行直到所有任务完成
DispatchQueue.global().asyncAfter(deadline: .now() + 10) {
    print("All downloads should be complete by now.")
    exit(0)
}

RunLoop.main.run()

DownloadOperation:这是一个自定义的 Operation 子类,代表一个下载任务。main 方法包含实际的下载逻辑。
DownloadQueueManager:这是一个单例类,用于管理高、中、低优先级的下载队列。每个队列使用不同的 qualityOfService 来代表优先级。
DownloadPriority:这是一个枚举,定义了三种下载优先级:高、中、低。
示例使用:创建了一些示例 URL,并将它们添加到不同优先级的下载队列中。最后使用 RunLoop.main.run() 保持程序运行,直到所有任务完成。

参考链接:

Swift 使用 Operation 实现
https://github.com/showmylym/FileDownloaderDemo

Swift 使用 sync/await 实现
https://www.jb51.net/program/2903196bf.htm

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容