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

推荐阅读更多精彩内容