美文网首页Swift
Swift进阶 - Concurrency之GCD

Swift进阶 - Concurrency之GCD

作者: Joshua666 | 来源:发表于2019-10-17 12:05 被阅读0次

    如果看完了之前写的Swift初学中的文章,应该对swift的基础有了一定的掌握。现在我们讨论一下进阶一些的知识,当你写一个比较复杂的app时,需要经常进行网络请求,就需要好好的架构你的app,不要让你的app因为长时间的请求出现卡顿的情况。这时候就需要考虑app的Concurrency。

    Swift提供了两个apis:GCDOperations,其中Operation是以GCD为基础实现的。

    一、GCD - Grand Central Dispatch

    let queue = DispatchQueue(label: "com.test.test")
    queue.async {
        let a = 5 + 5
        print(a)
        
        DispatchQueue.main.async {
            print("main\(a)")
        }
    }
    

    gcd就是把你写在closure里的代码放到队列中,这个队列根据你给的参数,可以是serial(串行)也可以是concurrent(并发)的。serial queue只有一个thread,所以这里面的任务每次执行一个,这一个结束了才会执行下一个;concurrent queue就会根据资源分配多个threads,可以同时执行多个任务。注意以下几点:

    1、GCD中的所有queue都是按照先进先出FIFO(first-in, first-out)的顺序,但是这指的是先进来的先执行,并不是先进来先完成,因为每个task的所需时间是不一样的;

    2、当你执行queue.async的时候,你的代码也不一定会并发进行!如果你的queue是一个serial queue,只有一个thread,你在怎么async,它也只能一个一个执行。

    3、label得是一个unique的string,你可以用“com.your-domain.xxx”的形式。

    4、当你的app开始运行时,系统会自动创建一个主queue,就是例子中的DispatchQueue.main,这是serial queue,主要是用来呈现UI的。不要把一个sync任务推给main,会卡UI的

    5、DispatchQueue默认会创建一个serial queue,你要这样定义来获取concurrent queue

    let queue = DispatchQueue(label: "com.test.test", attributes: .concurrent)
    

    6、Global concurrent queues

    当你需要concurrent queue时,你可以用上面的语句来定义一个你自己的,但是通常情况下,用系统本来就提供的global concurrent queue就行了:

    let queue = DispatchQueue.global(qos: .utility)
    

    qos(Quality of service)是指你这个queue中的任务的优先级,一共有6中:

    • .userInteractive - 这个任务与UI相关的时候,比如动画或者更新UI所需的逻辑运算等可以用这个,我们可不想因为复杂的运算影响UI的流畅性!
    • .userInitiated - 这个是当用户触发了一个事件,我们需要立即执行相关逻辑任务,但这个任务又可以并发进行的时候,比如用户点了一个button
    • .utility - 当你需要用一些progress bar或hud来显示加载中来执行此任务时,一般就是用这个了
    • .background - 当这个任务用户完全不需要知道
    • .default和.unspecified - 一般用不着,不讲了

    简单例子

    假设没有像SDWebImage的第三方库,你要写一个加载网络图片的uiCollectionView怎么写?这时候肯定要用到并发执行任务,不然肯定卡死(maybe not with 5g, but I don't care)

    func loadImage(indexPath: IndexPath) {
        let queue = DispatchQueue.global(qos: .utility) // a
        queue.async {
          [weak self] in // b
          guard let self = self else { // c
            return
          }
          if let data = try? Data(contentsOf: self.urls[indexPath.row]), 
            let image = UIImage(data: data) {
            DispatchQueue.main.async {
              if let cell = self.collectionView.cellForItem(at: indexPath) 
                        as? PhotoCell { // d
                cell.display(image: image)
              }
            }
          }
        }
      }
    

    a) 不知道图片都多大,所以也不知道加载一张图片需要多长时间,为了性能和电池,我们要用.utility
    b) 这里在gcd的async直接用self也不会出现retain cycle,因为closure在执行完之后会释放内存,但self会被延长“寿命”。
    c) 需要检查self是不是nil,万一加载出来之前self已经dismiss了。按照b里所说的,如果没用weak self,在加载出来前就不会dismiss
    d) 在async里,我们不能直接传入cell,因为当async里的代码执行时,你并不知道这个cell的状态,有可能已经没了,也有可能被换了,所以我们要传入indexpath,然后获取实时的cell

    7、Dispatch Group

    假设你需要同时向服务器进行多次请求,只有这几个请求都完成时,你才能更新UI,这时候你就用DispatchGroup来解决:

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .utility)
    queue.async(group: group) {
        print(2^5)
    }
    queue.async(group: group) {
        print(2^32)
    }
    group.notify(queue: DispatchQueue.main) {
        print("finished")
    
    

    finished只有在两个数都算完的时候才会被print。除了notify,group还有一个方法是wait - 如果用了group.wait(),它会把当前的queue给block,直到运行完group里的任务。wait还可以加一个timeout参数, 多少秒后,如果group中的任务还没执行完,那么正常继续执行当前queue后面的任务。举个例子就明白了

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .utility)
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(2))
        print("AAAAA")
    }
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(5))
        print("BBBBBB")
    }
    if group.wait(timeout: .now()+3) == .timedOut {
            print("不等了")
    }
    print("排队中...")
    // AAAAA
    // 不等了
    // 排队中...
    // BBBBBB
    

    注意print的顺序!不要在main queue用group.wait()

    8、@escaping

    当func的其中一个参数是closure的时候,有两种情况:
    a) 当func执行时会把closure的代码也执行,完成时,closure就不存在了

    func sum(_ arr: [Int], handler: (Int) -> ()) {
        print("doing sum...")
        let total = arr.reduce(0, +)
        handler(total)
        print("finish")
    }
    
    sum([1, 2, 3]) {
        total in
        print(total)
    }
    // doing sum...
    // 6
    // finish
    

    b) 当func执行完时,closure中的代码还没有执行完,这时候得让系统知道这个closure你要给我保存,不要在func执行完的时候把它从内存中毁掉,这时候就要加@escaping

    func sum(_ arr: [Int], handler: @escaping (Int) -> ()) {
        print("doing sum...")
        let total = arr.reduce(0, +)
        queue.async {
            handler(total)
        }
        print("finish")
    }
    
    sum([1, 2, 3]) {
        total in
        Thread.sleep(until: Date().addingTimeInterval(1))
        print(total)
    }
    // doing sum...
    // finish
    // 6
    

    9、group.enter()和group.leave()

    看完第7、8条后,再看看下面的代码,你觉得print出的顺序是怎样的?

    func sum(_ arr: [Int], handler: @escaping (Int) -> ()) {
        print("doing sum...")
        let total = arr.reduce(0, +)
        queue.async {
            handler(total)
        }
        print("finish")
    }
    
    queue.async(group: group) {
        sum(Array(1...2^32)) {
            total in
            Thread.sleep(until: Date().addingTimeInterval(2))
            print("total: \(total)")
        }
    }
    
    group.notify(queue: DispatchQueue.main) {
        print("都完成了")
    }
    

    我们在第7条中知道group.notify会在所有在group中的queue里的任务都完成之后执行。但是上面这个例子会以一下顺序print

    doing sum...
    finish
    都完成了
    total: 595
    

    因为queue执行的sum中还有一个async call,在这个async call还没完成的时候,notify就执行了!如果想解决这个问题,那么我们就可以用到group的两个方法:enter()leave()

    func sum(_ arr: [Int], handler: @escaping (Int) -> ()) {
        print("doing sum...")
        let total = arr.reduce(0, +)
        group.enter()
        queue.async {
            defer { group.leave() }
            handler(total)
        }
        print("finish")
    }
    // doing sum...
    // finish
    // total: 595
    // 都完成了
    

    在执行sum里的async之前,call了enter()告诉group我还有代码没完成
    a) 你要是call了enter()就要记得call leave()
    b) 因为有时候你还需要handle error在async的结果中,所以用了defer为了不漏写leave

    10、Semaphore

    如果资源有限,想限制可以并发进行的任务,可以用semaphore,用法如下:

    let semaphore = DispatchSemaphore(value: 2)
    for i in 1...5 {
        semaphore.wait()
        queue.async(group: group) {
            defer { semaphore.signal() }
            print("start\(i)")
            Thread.sleep(until: Date().addingTimeInterval(1))
            print("end\(i)")
        }
    }
    
    group.notify(queue: DispatchQueue.main) {
        print("all finished")
    }
    
    // print结果
    start1
    start2
    end1
    end2
    start3
    start4
    end4
    end3
    start5
    end5
    all finished
    

    相关文章

      网友评论

        本文标题:Swift进阶 - Concurrency之GCD

        本文链接:https://www.haomeiwen.com/subject/rzrmmctx.html