介绍
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) }
在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
修饰,闭包默认是不逃逸的。需要注意的是在闭包在回调的时候需要手动键入回调回来的返回值形参,例如以下图片中的方式:
打印结果输出:
尾随闭包
如果函数的最后一个参数是闭包那么该函数参数可以提前结束,最后一个参数可以直接使用 {}
包装(闭包里嵌套尾随闭包除外)。
以下为闭包的完整写法:
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-C
的 Block
时,当某一个类拥有该 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
方法;