Swift闭包

  • 闭包的定义

    闭包是一个捕获上下文的常量或变量的匿名函数
func test() {
    print("test")
}

👆的全局函数是一种特殊闭包不捕获变量;

👇的内嵌函数也是一个捕获外部变量闭包

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    // 内嵌函数,会捕获外层函数的变量runningTotal
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
  • 闭包的写法

闭包表达式:会自动上下文捕获(常)变量

{ (parameters) -> returntype in
    statements
}
  
//将闭包声明为可选类型
var closure : ((Int) -> Int)?
closure = nil

//将闭包表达式赋值给常量
let closure : (Int) -> Int
closure = {(age : Int) -> Int in
    return age
}
//或者省略 -> Int
closure = {(age : Int) in
    return age
}

// 闭包作为函数参数
func test(param : () -> Int) {
    print(param())
}
// 使用尾随闭包
var age = 10
test { ()->Int in
   age += 1
   return age
}

使用闭包表达式好处

  1. 利用上下文推断参数返回值类型
  2. 单一表达式可以隐式返回(省略return关键字)
  3. 简写参数名称($0)
  4. 尾随闭包表达式增加代码的可读性
  • 尾随闭包

1.将闭包表达式作为函数的最后一个参数时,如果当前闭包表达式很长,则可以通过尾随闭包的方式来提高代码的可读性

func test(_ a : Int, _ b : Int, _ c : Int, by : (_ item1 : Int, _ item2 : Int, _ item3 : Int) -> Bool) -> Bool {
    return by(a, b, c)
}

// 普通调用:可读性差
test(2, 3, 4, by: {(_ item1 : Int, _ item2 : Int, _ item3 : Int) -> Bool in
    return (item1 + item2 < item3)
}

// 通过尾随闭包调用:简单明了
test(2, 3, 4) {(_ item1 : Int, _ item2 : Int, _ item3 : Int) -> Bool in
    return (item1 + item2 < item3)
}

2.当函数有且仅有闭包表达式作为唯一参数时,可以省略小括号直接写闭包表达式。

应用:简化array.sort的调用语法

var array = [5,1,3]

// 普通调用
array.sort(by: {(item1 : Int, item2 : Int) -> Bool in return item1 < item2 })
// 省略参数类型:闭包表达式可以根据上下文推断参数类型
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
// 闭包表达式作为唯一参数时省略(),直接{}
array.sort {(item1, item2) -> Bool in return item1 < item2 }
// 省略返回值类型:闭包表达式可以根据上下文推断返回值类型
array.sort{(item1, item2) in return item1 < item2 }
// 省略return关键字:单一表达式隐式返回
array.sort{(item1, item2) in item1 < item2 }
// 用$0、$1简写参数
array.sort{$0 < $1 }
// 简化语法终极版
array.sort(by: <)
  • 闭包的本质:捕获值

首先通过之前的例子来分析闭包:

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

通过两种不同的调用方式打印结果:
方式一:

print(makeIncrementer()()) //11
print(makeIncrementer()()) //11
print(makeIncrementer()()) //11

和方式二:

let makeInc = makeIncrementer()
print(makeInc()) //11
print(makeInc()) //12
print(makeInc()) //13

显然上面两种调用方式得到了两种不同的结果:方式一中是每次都重新分配一个变量runningTotal,其值为10,每次调用都是在10的基础上+1,所以得出的结果都是11;而方式二中,调用一次外层函数makeIncrementer就只分配了一个变量runningTotal = 10,函数的返回结果makeInc捕获了变量runningTotal,意味着runningTotal就不再是一个单纯的值10了,而是一个引用地址,所以每次调用就是进行引用类型的赋值操作。

SIL中分析闭包捕获值的本质:

// makeIncrementer()
sil hidden @main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) () -> @owned @callee_guaranteed () -> Int {
bb0:
  // 通过 alloc_box 在堆上分配一块内存,存储metadata、refCount、value,内存地址--->runningTotal
  %0 = alloc_box ${ var Int }, var, name "runningTotal" // users: %8, %7, %6, %1
  // 通过 project_box 取出%0(理解为对象)的value的地址
  %1 = project_box %0 : ${ var Int }, 0           // user: %4
  %2 = integer_literal $Builtin.Int64, 10         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  // 将10存入%1这个地址里
  store %3 to %1 : $*Int                          // id: %4
  // function_ref incrementer #1 () in makeIncrementer()
  %5 = function_ref @incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %7
  // 闭包调用前对%0(理解为对象)做retain操作
  strong_retain %0 : ${ var Int }                 // id: %6
  // 调用内嵌函数incrementer(闭包)
  %7 = partial_apply [callee_guaranteed] %5(%0) : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %9
   // 闭包调用后对%0(理解为对象)做release操作
  strong_release %0 : ${ var Int }                // id: %8
  return %7 : $@callee_guaranteed () -> Int       // id: %9
} // end sil function 'main.makeIncrementer() -> () -> Swift.Int'
捕获runningTotal的图解

汇编模式下也可看出调用了swift_allocObject

总结:
1.综上可知闭包捕获值的本质就是在上分配一块内存空间存储值的引用
2.闭包能从上下文捕获已被定义的(常)变量,即使它们的原作用域已经不存在,闭包仍能在其函数体内引用修改这些值;
3.每次修改的捕获值其实修改的是堆内存中的value值;
4.每次重新执行当前函数时都会重新创建内存空间。

  • 闭包是引用类型

接下来通过分析IR文件来看一下makeInc这个变量中到底存储的是什么?

  • 简单的IR语法
  • 数组
<elementnumber> x <elementtype>
iN: N位(bits)的整型
// example 
alloca [24 x i8], align 8  //24个i8(1字节的整型)都是0
  • 结构体
// 名称 = type {结构体成员,类型}
%swift.refcounted = type { %swift.type*, i64 } 
//example
%T = type {<type list>} // 类似C的结构体
  • 指针
<type> *
//example
i64*   //8字节的整型指针
  • getelementptr指令
    用于LLVM中获得指向数组的元素和指向结构体成员的指针
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*

通过LLVM官网的例子来熟悉IR语法:

struct munger_struct {
    int f1;
    int f2;
};

void munge(struct munger_struct *P) {
    P[0].f1 = P[1].f1 + P[2].f2;
}
  • %2: 创建一块内存空间存放数组首地址(数组中存放[结构体1的地址,结构体2的地址,结构体3的地址])--->%struct.munger struct** %2访问二级指针

  • load %struct.munger struct*, %struct.munger struct**%2, align 8----->取出二级指针%2中的地址返回(数组的首地址

  • getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %3, i64 1第一层索引i64 1,相对于自身(这里是数组)偏移1个i64大小的地址即arr[1]的地址

  • getelementptr inbounds %struct.munger_struct, %struct.munger_struct* %4, i32 0, i32 0:第一层索引i32 0相对于自身(使用的基本类型结构体)偏移0个i32大小的地址(还是arr[0]这个结构体的首地址),第二层i32 0相对于结构体中的元素偏移0个i32大小的地址(结构体中第一个元素f1的地址 *i32)

总结:
1.getelementptr后面可以有多层索引,每多一层索引,索引使用的基本类型和返回的指针类型去掉一层;
如上面的i32 0, i32 0:第一层i32 0使用的基本类型是结构体类型(所以相对于自身偏移),返回的是结构体指针类型(拿到的是结构体的地址);第二层i32 0使用的基本类型是int(结构体去掉一层),返回的是*Int(结构体指针去掉一层)。

  1. 第一层索引会改变getelementptr这个表达式的返回类型(因为是相对于自身偏移);

3.第一层索引的偏移量由第一层索引的值和第一层ty指定的基本类型决定的。

P[1].f1
P[2].f2
P[0].f1
  • 分析闭包的数据结构
  1. 只有一个捕获值时:
    上面例子生成的IR文件中,makeIncrementer函数的实现如下:
%swift.full_boxmetadata = type { void (%swift.refcounted*)*, i8**, %swift.type, i32, i8* }
%swift.refcounted = type { %swift.type*, i64 }
%swift.type = type { i64 }
%TSi = type <{ i64 }>

define hidden swiftcc { i8*, %swift.refcounted* } @"main.makeIncrementer() -> () -> Swift.Int"() #0 {
entry:
  %runningTotal.debug = alloca %TSi*, align 8
  %0 = bitcast %TSi** %runningTotal.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)
  %1 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1
  %2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
  %3 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %2, i32 0, i32 1
  %4 = bitcast [8 x i8]* %3 to %TSi*
  store %TSi* %4, %TSi** %runningTotal.debug, align 8
  %._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
  store i64 10, i64* %._value, align 8
  %5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %1) #1
  call void @swift_release(%swift.refcounted* %1) #1
  %6 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int" to i8*), %swift.refcounted* undef }, %swift.refcounted* %1, 1
  ret { i8*, %swift.refcounted* } %6
}

bitcast: 按位转换,转换前后的类型的位大小必须相同;类似unsafeBitCast
%1: 通过swift_allocObject分配的内存,返回swift.refcounted*类型
%2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*: 将swift.refcounted*类型按位转换为{ %swift.refcounted, [8 x i8] }结构体类型
%3: 获取结构体中的[8 x i8]
%4: 将[8 x i8]* 按位转换---> %TSi* (%TSi = type <{ i64 }>)即i64*,存储runningTotal的值
%._value: 偏移后的地址就是%4这个i64*地址,将i64类型的10存储在runningTotal
{ i8*, %swift.refcounted* }: 最终返回的值
i8*--->void *:存储内嵌函数的地址
%swift.refcounted*:存储 %1({ %swift.refcounted, [8 x i8] }>*

  • 模拟闭包的数据结构
func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
 
        return runningTotal
    }
    return incrementer
}

/**
 %swift.refcounted = type { %swift.type*, i64 }
 %swift.type = type { i64 }
 %swift.refcounted的数据结构
 */
struct HeapObject {
    var type : UnsafePointer<Int64>
    var refCounts : Int64
}

/**
 bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
 { %swift.refcounted, [8 x i8] }的数据结构
 T : 可传入其它类型
 */
struct BoxValue<T> {
    var heapObj : HeapObject
    var value : T
}

/**
 ret { i8*, %swift.refcounted* } %6
 { i8*, %swift.refcounted* }的数据结构
 */
struct FuncResult<T> {
    var funcAddress : UnsafeRawPointer // 内嵌函数的地址
    var captureValue : UnsafePointer<BoxValue<T>> // 捕获值的地址
}

// 返回类型(包装的结构体,直接传入函数类型的话在转换过程中会出错)
struct Function {
    var function : () -> Int
}

// 将Function<T>绑定到FuncResult<T>的数据结构
var makeInc = Function(function: makeIncrementer())

let funcPtr = UnsafeMutablePointer<Function>.allocate(capacity: 1)
funcPtr.initialize(to: makeInc)

let bindPtr = funcPtr.withMemoryRebound(to: FuncResult<Int>.self, capacity: 1) { p in
    return p.pointee
}

funcPtr.deinitialize(count: 1)
funcPtr.deallocate()

print(bindPtr.funcAddress) //0x0000000100002b80
print(bindPtr.captureValue.pointee.value) //10
通过终端查看内存地址

bindPtr.funcAddress内嵌函数地址对应的符号

  1. 两个捕获值时闭包的数据结构:
func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    var value = 6.00
    func incrementer() -> Int {
        runningTotal += 1
        value += 4.00
        return runningTotal
    }
    return incrementer
}

可知上面的闭包捕获两个值:Int--> runningTotal和 Double--> value,数据结构稍微有一些改变,中间多了一层封装两个捕获值:

/**
<{ %swift.refcounted, %swift.refcounted*, %swift.refcounted* }>*
%swift.refcounted*: 第一个捕获值的地址 T:第一个捕获值的类型
%swift.refcounted*: 第二个捕获值的地址 V:第二个捕获值的类型
*/
struct MultiBox<T, V> {
    var refCounted : HeapObject
    var value1 :  UnsafePointer<BoxValue<T>>
    var value2 :  UnsafePointer<BoxValue<V>>
}

/**
 ret { i8*, %swift.refcounted* } %6
 { i8*, %swift.refcounted* }的数据结构
 */
struct FuncResult<T, V> {
    var funcAddress : UnsafeRawPointer // 内嵌函数的地址
    var captureValue : UnsafePointer<MultiBox<T, V>> // 捕获值的地址
}

print(bindPtr.funcAddress) //0x0000000100002750
print(bindPtr.captureValue.pointee.value1.pointee.value) //10
print(bindPtr.captureValue.pointee.value2.pointee.value) //6.0

总结:

  • 闭包捕获值的原理就是在上开辟内存空间,存储捕获值
  • 修改捕获值时就是去堆上修改
  • 闭包是引用类型(地址传递),闭包的底层结构-->结构体(函数的地址 + 捕获变量的值)
  • 闭包不捕获值时(函数)也是一个引用类型(地址传递),底层结构还是一个结构体(函数地址 + null),只保存了函数地址。
func makeIncrementer(inc : Int) -> Int {
    var runningTotal = 10
    runningTotal += inc
    return runningTotal
}

var makeInc = makeIncrementer
 
struct FuncData {
    var funcAddress : UnsafeRawPointer
    var captureValue : UnsafeRawPointer?
}

struct Func {
    var function : (Int) -> Int
}

var f = Func(function: makeIncrementer(inc:))
let ptr = UnsafeMutablePointer<Func>.allocate(capacity: 1)
ptr.initialize(to: f)

let funcPtr = ptr.withMemoryRebound(to: FuncData.self, capacity: 1){
    $0.pointee
}

ptr.deinitialize(count: 1)
ptr.deallocate()

print(funcPtr.funcAddress) //0x00000001000030a0
print(funcPtr.captureValue) //nil

/**
cat address 0x0000000100003000
&0x0000000100003000,  FirstSwiftTest.makeIncrementer(inc: Swift.Int) -> Swift.Int <+0> , ($s14FirstSwiftTest15makeIncrementer3incS2i_tF)FirstSwiftTest.__TEXT.__text
*/
  • 逃逸闭包

1.定义
  • 将闭包作为实际参数传递给函数
  • 该闭包是在函数返回后调用
    • 异步延迟调用
    • 存储,需要时再调用
2. 声明:逃逸闭包前+@escaping
var completionHandler : ((Int)->())?

// 先存储再调用
func makeIncrementer(amount : Int, handler : @escaping (Int) -> ()) {
    var runningTotal = 10
    runningTotal += amount
    // 必须显式引用self,目的是提醒可能有循环引用,自己注意解决
    self.completi
onHandler = handler
}

// 异步延迟调用
func async_makeIncrementer(amount : Int, handler : @escaping (Int) -> ()) {
    var runningTotal = 10
    runningTotal += amount
    
    DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) {
        handler(runningTotal)
    }
}

重点:逃逸闭包的生命周期一定要比函数的生命周期

3.区别非逃逸闭包
  • 非逃逸闭包和逃逸闭包的前提都是:作为函数的参数

  • 非逃逸闭包在sil中都是@noescape修饰

  • 函数体内执行,生命周期函数生命周期一致

  • 非逃逸闭包会产生循环引用(作用域在函数作用域内)

  • 编译器对非逃逸闭包优化:省略掉内存管理retain、release

  • 逃逸闭包耗资源,不要滥用

  • 逃逸闭包必须显式引用self,提醒可能存在循环引用需要解决。

  • 自动闭包

    • 使用 @autoclosure关键字声明自动闭包
    • 接收任何参数返回值是当前内部表达式的值
func debugErrorMsg(_ condition : Bool, _ message : @autoclosure () -> String) {
    if condition {
        print(message())
    }
}

debugErrorMsg(true, "NetWork Error Occured")

上面例子中,实际就是将传入的字符串"NetWork Error Occured"放入到一个闭包表达式中,在调用的时候返回。即:

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

推荐阅读更多精彩内容

  • Swift 闭包 [TOC] 前言 我个人觉得在看这篇文章前,先了解一下Swift 函数[https://swif...
    just东东阅读 730评论 0 2
  • 前言 本篇文章主要讲解Swift中又一个相当重要的知识点 👉 闭包,首先会介绍闭包的概念,包含与OC中Block的...
    深圳_你要的昵称阅读 473评论 0 9
  • 前情提要 Swift的闭包和OC的Block是一回事,是一种特殊的函数-带有自动变量的匿名函数。 分别从语法和原理...
    Jacob6666阅读 408评论 0 0
  • 什么是闭包 维基百科中的解释:在计算机科学中,闭包(Closure),又称词法闭包(Lexical Closure...
    帅驼驼阅读 401评论 0 3
  • 本文主要分析闭包以及闭包捕获变量的原理 闭包 闭包是一个捕获了全局上下文的常量或者变量的函数,通俗来讲,闭包可以是...
    辉辉岁月阅读 320评论 0 0