Swift的闭包函数

闭包函数的结构

介绍

Swift官方开发文档对于闭包的介绍是这样的:闭包是可以在代码中被传递和引用的功能性独立模块。Swift 中的闭包类似于其他语言的匿名函数,和 C Objective-C 中的 Block 很像。主要作用是捕捉和存储定义在上下文中的任何常量和变量的引用,处理关于捕获的内存管理的操作(看了这些介绍很懵逼,我只看懂了一句”和 Block 很像“)。

闭包的应用场景

  • 异步执行完成回调
  • 控制器间回调
  • 自定义视图回调

闭包的定义

闭包有特殊的定义方式,定义格式为 {参数列表 -> 返回值 in 实现代码},如果没有参数也没有返回值可以省略前面的定义部分(同时省略关键字 in)直接写实现代码。

  • 没有参数没有返回值的闭包(最简单的闭包)
    override func viewDidLoad() {
       super.viewDidLoad()
       demo()
    }
    func demo() {
       let a = {
          print("这是个最简单的闭包")
       }
       // 执行闭包
       a ()
    }
    
    从上面代码中可以可以看出,Swift 中闭包的写法与 Objective-C 中的 Block 写法基本一致,只是少了一个 ^ 符号。
    闭包常量的类型推断

    从类型推断可以看出函数可以作为参数传递,并且每个函数都是一个特殊的闭包。
    结果输出:
    简单闭包函数输出
  • 有参数的闭包
    override func viewDidLoad() {
       super.viewDidLoad()
       demo()
    }
    func demo() {
       let sum = {
          (a : Int, b : Int) in
          print("a = \(a), b = \(b)")
       }
       sum(10, 20)
    }
    
    有参数的闭包传值输出

    有参闭包在Xcode中的表现

    Swift 闭包中,参数、返回值和实现代码都要写在 {} 中,需要关键字 in 分隔定义和实现。
  • 有参数也有返回值的闭包
    override func viewDidLoad() {
       super.viewDidLoad()
       demo()
    }
    func demo() {
       let sum = {
          (a : Int, b : Int) -> Int in
          return a + b
       }
       print(sum(10, 20))
    }
    
    有参数和返回值闭包的输出结果

使用闭包回调传递参数

在异步执行任务获取结果通过闭包回调,与 Objective-C 中的 Block 的用法完全一致。

override func viewDidLoad() {
   super.viewDidLoad()
   loadData { 
            (result) in
            print(result)
        }
}
func loadData(completion: @escaping ([String])  -> Void) -> () {
    DispatchQueue.global().async {
        print("此处执行耗时操作\(Thread.current)")
        // 模拟耗时操作,让线程休眠10秒钟
        Thread.sleep(forTimeInterval: 10)
        // 异步加载网络数据(这里只做演示)
        let jsonData = ["八卦", "头条", "新闻"]
            
        // 回到主线程更新界面
        DispatchQueue.main.async(execute: {
            // 回调执行闭包,传递异步获取到的结果
            completion(jsonData)
        })
    }
}

以上代码为 Swift4.0 的最新写法,在早期的 Swift 中闭包可以使用外部参数,但在 Swift3.0 更新了以后苹果禁止了闭包设置外部参数,并且抛弃了 @noescaping 的使用改用 @escaping,如果闭包的回调需要逃逸必须使用@escaping 修饰,闭包默认是不逃逸的。需要注意的是在闭包在回调的时候需要手动键入回调回来的返回值形参,例如以下图片中的方式:

Swift4.0语法强调

闭包回调接收值的类型推导

打印结果输出:
使用闭包回调的输出结果

尾随闭包

如果函数的最后一个参数是闭包那么该函数参数可以提前结束,最后一个参数可以直接使用 {} 包装(闭包里嵌套尾随闭包除外)。
以下为闭包的完整写法:

override func viewDidLoad() {
   super.viewDidLoad()
   loadData(completion : {
      (result) -> () in
      print(result)
   })
}
func loadData(completion : @escaping ([String]) -> ()) -> () {
   DispatchQueue.Global().async {
      print("此处执行耗时操作\(Thread.current)")
      // 模拟耗时操作,让线程休眠 10 秒钟
      Thread.sleep(forTimeInterval: 10)
      // 此处模拟从网络获取获取数据
      let resultData = ["八卦", "头条", "新闻"]
      // 回到主线程更新UI
      DispatchQueue.main.async(execute : {
         // 回调执行闭包,传递异步获取到的网络数据
         completion(resultData)
      })
   }
}
尾随闭包的原始写法

以下为尾随闭包的简写版:

override func viewDidLoad() {
   super.viewDidLoad()
   loadData{
      (result) in
      print(result)
   }
}
func loadData(completion : @escaping ([String]) ->()) {
   DispatchQueue.Global().async {
      print("此处执行耗时操作\(Thread.current)")
      // 模拟耗时操作,让线程休眠 10 秒钟
      Thread.sleep(forTimeInterval: 10)
      // 此处模拟网络获取到的数据
      let resultData = ["八卦", "头条", "新闻"]
      
      // 回到主线程更新 UI
      DispatchQueue.main.async {
         // 回调执行闭包,传递异步获取到的网络数据
         completion(resultData)
      }
   }
}
尾随闭包的简写

输出结果:


尾随闭包的输出结果

闭包中出现循环引用的解决方式

做苹果平台软件的开发者对于循环引用都不会陌生,在使用 Objective-CBlock 时,当某一个类拥有该 block 块并且在本类中会使用该 block 块进行操作的时候,如果在这个 block 块中使用 self 会形成循环引用(这段话写的也是啰嗦到没谁了),不啰嗦了直接上代码:

#import "ViewController.h"

@interface ViewController ()
// 定义 Block 属性
@property (nonatomic, copy) void (^testBlock) (void);
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 调用方法
    [self loadData: {
         NSLog(@"%@", self.view);
    }];
}
- (void)loadData:(void (^) (void))completion {
     // 使用属性记录 block
     self.testBlock = completion;
     dispatch_async(dispatch_get_global_queue(0, 0), ^{
          NSLog(@"耗时操作");
          [NSThread sleepForTimeInterval: 2.0];
          dispatch_async(dispatch_get_main_queue(), ^{
               //执行block
               completion();
          });
     });
}
@end

以上代码是最典型的循环引用代码,可以看到控制器定义 ```block属性拥有该block块,又在block实现中使用强引用self造成block引用控制器,以下为Objective-C`的循环引用解决方式:

#import "ViewController.h"

@interface ViewController ()
// 定义 Block 属性
@property (nonatomic, copy) void (^testBlock) (void);
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 此处打断循环
    __weak typeof(self) weakSelf = self;
    // 调用方法
    [self loadData: {
         NSLog(@"%@", weakSelf.view);
    }];
}
- (void)loadData:(void (^) (void))completion {
     // 使用属性记录 block
     self.testBlock = completion;
     dispatch_async(dispatch_get_global_queue(0, 0), ^{
          NSLog(@"耗时操作");
          [NSThread sleepForTimeInterval: 2.0];
          dispatch_async(dispatch_get_main_queue(), ^{
               //执行block
               completion();
          });
     });
}
@end

前面说了在 Swift 中闭包与 block 的应用场景完全一致,那么在 Swift 中出现循环引用该如何解决呢?

Swift 中循环引用的解决方式

  • 使用Objective-C的方法解决Swift循环引用
    import UIKit
    
    class ViewController: UIViewController {
        // 定义一个闭包属性
        var completionBack : (() -> ())?
     
        override func viewDidLoad() {
            super.viewDidLoad()
            // 调用函数
            loadData {
                print(self.view)
            }
        }
        // 定义带有闭包的函数
        func loadData(completion : @escaping () -> ()) -> () {
         
            // 使用属性记录闭包
            completionBack = completion
         
            DispatchQueue.global().async {
                print("耗时操作")
                Thread.sleep(forTimeInterval: 2.0)
                DispatchQueue.main.async {
                    // 回调 执行闭包
                    completion()
                }
            }
        }
        deinit {
            print("视图销毁了")
        }
     }
    
    以上代码中出现的问题与 Objective-C 中的循环引用一样,因为形成了循环引用该视图控制器中的 deinit 永远不会被调用,这样就形成了内存泄漏从而导致 App 闪退等问题。接下来通过内存图可以看到视图的引用情况:
    循环引用的内存表现图

    下面是按照 Objective-C 的解决思路来解决该问题:
    import UIKit
    
    class ViewController: UIViewController {
        // 定义一个闭包属性
        var completionBack : (() -> ())?
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // 此处打断循环链条
            weak var weakSelf = self
            // 调用函数
            loadData {
                print(weakSelf?.view ?? "")
            }
        }
        // 定义带有闭包的函数
        func loadData(completion : @escaping () -> ()) -> () {
         
            // 使用属性记录闭包
            completionBack = completion
         
            DispatchQueue.global().async {
                print("耗时操作")
                Thread.sleep(forTimeInterval: 2.0)
                DispatchQueue.main.async {
                    // 回调 执行闭包
                    completion()
                }
            }
        }
        deinit {
            print("视图销毁了")
        }
     }
    
    当我们在 Swift 中使用 weak 的时候要特别注意,该关键字只能修饰通过 var 定义的变量,原因是系统会在运行时对该变量进行修改。
    用内存表现图来说明解决后的内存占有情况:
    解除循环引用后的内存表现图
  • Swift 推荐解除循环引用
    import UIKit
    
    class ViewController: UIViewController {
        // 定义一个闭包属性
        var completionBack : (() -> ())?
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // 调用函数
            loadData { 
                [wead self] in
                print(self?.view ?? "")
            }
        }
        // 定义带有闭包的函数
        func loadData(completion : @escaping () -> ()) -> () {
         
            // 使用属性记录闭包
            completionBack = completion
         
            DispatchQueue.global().async {
                print("耗时操作")
                Thread.sleep(forTimeInterval: 2.0)
                DispatchQueue.main.async {
                    // 回调 执行闭包
                    completion()
                }
            }
        }
        deinit {
            print("视图销毁了")
        }
     }
    
    这份代码只修改了函数的调用部分,在闭包函数里添加代码 [weak self] in 表示该闭包里使用的所有 self 都是弱引用,需要注意的是可选值的解包。
    Swift 还提供了另外一种解除循环引用的方式,将上述代码中的 [weak self] in 替换为 [unowned self] in。此时的闭包中 self 不是可选值不用考虑解包的问题,但这行代码与 Objective-C中的__unsafe_unretained的功能一样。表示该闭包中使用的所有 self 都是 assign 值,虽然不会强引用,但在对象释放了之后 self 的指针地址不会置空,如果对象被释放了以后继续调用就会出现野指针,操作的时候会让程序crash,所以这种方式相比 [weak self] in 并不安全,只需要简单了解一下即可。

总结

  • Swift 中的闭包与 Block 的应用场景一样;
  • 每个函数都是一个特殊的闭包;
  • 闭包函数有特殊的写法 {参数列表 -> 返回值类型 in 实现代码},如果没有参数也没有返回值可省略前面定义部分;
  • Swift4.0 更新了闭包定义的语法格式,抛弃了 @noescaping 的使用;
  • 函数中的最后一个参数是闭包,那么该闭包称为尾随闭包,尾随闭包可省略函数的形参直接使用 {} 包装编写。
  • Swift 中提供了两种解除循环引用的方式,推荐使用 [weak self] in 方法;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容