按url的顺序下载图像-iOS Swift

时间:2019-09-14 18:24:21

标签: ios swift multithreading grand-central-dispatch semaphore

我在一个数组中有10个网址,当下载了4个网址时,我需要显示它们。我使用信号量和组来实现。但看起来我陷入僵局。不知道如何进行。请提供建议

在操场上模拟它:

PlaygroundPage.current.needsIndefiniteExecution = true

let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInteractive)
let semaphore = DispatchSemaphore(value: 4)
var nums: [Int] = []
for i in 1...10 {

    group.enter()
    semaphore.wait()
    queue.async(group: group) {
        print("Downloading image \(i)")
        // Simulate a network wait
        Thread.sleep(forTimeInterval: 3)
        nums.append(i)
        print("Hola image \(i)")
        if nums.count == 4 {
            print("4 downloaded")
            semaphore.signal()
            group.leave()
        }
    }
    if nums.count == 4 {
        break
    }
}

group.notify(queue: DispatchQueue.main) {
    print(nums)
}

我在o / p控制台中得到了

> Downloading image 1
> Downloading image 2
> Downloading image 3
> Downloading image 4
  

信号量(41269,0x70000ade5000)malloc:对象0x1077d4750的错误:未分配要释放的指针

     

Semaphores(41269,0x70000ade5000)malloc:***在malloc_error_break中设置一个断点进行调试

我希望按顺序打印[1,2,3,4]

我知道我正在尝试异步访问共享资源,但不确定如何解决此问题。请指教

如果我想一次下载4,4,2个任务,使其在[symphore]中显示[1,2,3,4,5,6,7,8,9,10],该如何与信号量一起使用我的输出

1 个答案:

答案 0 :(得分:0)

您的标题为“按url顺序下载图像”,但是您的代码段并未尝试这样做。似乎正在尝试使用信号量来将下载一次限制为四张图片,但这并不能保证它们会按顺序排列。

值得称赞的是,该代码段并未尝试依次下载它们,因为这会带来巨大的性能损失。很好的是,此代码片段将并发程度限制为合理的程度,从而避免了耗尽工作线程或导致某些后继请求超时。因此,使用信号量允许并发图像下载,但一次将其限制为四个的想法是一种很好的方法。我们只需要在结果排序时将结果排序。

但是在开始之前,让我们解决提供的代码段中的许多问题:

  1. 您每次迭代都调用group.enter()semaphore.wait()(这是正确的),但是只有在group.leave()为{时,才semaphore.signal()i {1}}(不正确)。您希望每次迭代4leave

    很显然,也不需要进行signal调用。

    因此,要解决此“一次执行四个”过程,可以简化此代码:

    break

    这将一次下载四张图像,并在完成后调用let group = DispatchGroup() let queue = DispatchQueue.global(qos: .userInteractive) let semaphore = DispatchSemaphore(value: 4) var nums: [Int] = [] for i in 1...10 { group.enter() semaphore.wait() queue.async() { // NB: the `group` parameter is not needed print("Downloading image \(i)") // Simulate a network wait Thread.sleep(forTimeInterval: 3) nums.append(i) print("Hola image \(i)") semaphore.signal() group.leave() } } group.notify(queue: .main) { print(nums) } 关闭。

  2. 尽管以上内容修复了信号量和组逻辑,但上面的代码片段中还隐藏着另一个问题。它正在从多个后台线程更新group.notify数组,但是nums不是线程安全的。因此,您应该将这些更新同步到该阵列。一种简单的方法是将更新分派回主线程。 (任何串行队列都可以,但是主线程可以正常工作。)

    此外,由于永远不要在主队列上调用Array,因此建议您将整个wait循环显式分派到后台线程:

    for

    现在正确的做法是“一次执行四个,然后在完成时通知我。”


好的,既然我们已经正确下载了所有图像,让我们弄清楚如何对结果进行排序。坦白说,如果我们认为我们有某种图像下载方法(例如这样)可以下载特定图像,那么我会更容易理解发生的事情:

DispatchQueue.global(qos: .utility).async {
    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInteractive)
    let semaphore = DispatchSemaphore(value: 4)
    var nums: [Int] = []

    for i in 1...10 {
        group.enter()
        semaphore.wait()

        queue.async() {
            print("Downloading image \(i)")
            // Simulate a network wait
            Thread.sleep(forTimeInterval: 3)
            DispatchQueue.main.async {
                nums.append(i)
                print("Hola image \(i)")
            }

            semaphore.signal()
            group.leave()
        }
    }

    group.notify(queue: .main) {
        print(nums)
    }
}

然后执行以下程序:(a)一次下载图像,一次最多下载四张;和(b)将结果按顺序返回,看起来可能像这样:

func download(_ url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) { ... }

您会这样称呼它:

func downloadAllImages(_ urls: [URL], completion: @escaping ([UIImage]) -> Void) {
    DispatchQueue.global(qos: .utility).async {
        let group = DispatchGroup()
        let semaphore = DispatchSemaphore(value: 4)
        var imageDictionary: [URL: UIImage] = [:]

        // download the images

        for url in urls {
            group.enter()
            semaphore.wait()

            self.download(url) { result in
                defer {
                    semaphore.signal()
                    group.leave()
                }

                switch result {
                case .failure(let error):
                    print(error)

                case .success(let image):
                    DispatchQueue.main.async {
                        imageDictionary[url] = image
                    }
                }
            }
        }

        // now sort the results

        group.notify(queue: .main) {
            completion(urls.compactMap { imageDictionary[$0] })
        }
    }
}

FWIW,“下载单个图像”例程可能类似于:

downloadAllImages(urls) { images in
    self.images = images
    self.updateUI()        // do whatever you want to trigger the update of the UI
}

这是使用Swift 5 enum DownloadError: Error { case notImage case invalidStatusCode(URLResponse) } func download(_ url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) { URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data, let response = response as? HTTPURLResponse, error == nil else { completion(.failure(error!)) return } guard 200..<300 ~= response.statusCode else { completion(.failure(DownloadError.invalidStatusCode(response))) return } guard let image = UIImage(data: data) else { completion(.failure(DownloadError.notImage)) return } completion(.success(image)) } } 枚举。如果您使用的是较早版本的Swift,则可以自己定义此枚举的简单形式:

Result

最后,值得注意其他一些选择:

  1. 将网络请求包装在异步enum Result<Success, Failure> { case success(Success) case failure(Failure) } 子类中,并将其添加到Operation设置为4的操作队列中。如果您对此方法感兴趣,我可以提供一些参考

  2. 使用Kingfisher之类的图像下载库。

  3. 使用maxConcurrentOperationCount扩展名(例如Kingfisher提供)代替完全下载所有图像,而完全放弃“下载所有图像”过程,并转到一种模式只需指示您的图像视图以即时方式(或预取)异步检索图像即可。