函数类型
函数类型是又 函数参数类型 和 函数返回值类型 构成的
来个例子,以下函数赋值给变量a,完全没问题
func addTwoInts(_ a:Int, _ b:Int) -> Int{
return a + b
}
var a = addTwoInts
但是如果有两个同名函数,就有问题了,编译器不知道你指的哪一个函数
func addTwoInts(_ a:Int, _ b:Int) -> Int{
return a + b
}
func addTwoInts(_ a:Double, _ b:Double) -> Double{
return a + b
}
var a = addTwoInts
所以我们需要给变量指定函数类型,也就是指定 函数参数类型 和 函数返回值类型
var a:(Double, Double) -> Double = addTwoInts
函数是引用类型
变量a存储的函数,本质上是存储了一个metadata,存放在mach-o文件的__DATA_CONST段中。也就是说函数在赋值给变量a的时候,是把函数的metadata地址,赋值给了a,然后从b的第一个八字节看出,里面存放了跟a一样的metadata。所以swift中的函数,也是一个引用类型。
探究函数metadata
打开swift源码,到Metadata.h,找到TargetFunctionTypeMetadata,它就是函数的metadata结构体,继承于TargetMetadata。
继承于TargetMetadata里面有kind,也就是isa,TargetFunctionTypeMetadata里面有Flags,看下Flags里面都是什么
通过flags标识了这个函数的类型。
还有一个参数是ResultType,是函数的返回值类型
通过分析,可以将函数的metadata还原成以下swift代码
struct TargetFunctionTypeMetadata{
var kind: Int
var flags: Int
var arguments: ArgumentsBuffer<Any.Type>
func numberArguments() -> Int{
return self.flags & 0x0000FFFF
}
}
struct ArgumentsBuffer<Element>{
var element: Element
mutating func buffer(n: Int) -> UnsafeBufferPointer<Element> {
return withUnsafePointer(to: &self) {
let ptr = $0.withMemoryRebound(to: Element.self, capacity: 1) { start in
return start
}
return UnsafeBufferPointer(start: ptr, count: n)
}
}
mutating func index(of i: Int) -> UnsafeMutablePointer<Element> {
return withUnsafePointer(to: &self) {
return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
}
}
}
//获取addTwoInts的类型: ((Double, Double) -> Double).Type
let value = type(of: addTwoInts)
let functionType = unsafeBitCast(value as Any.Type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
print(functionType.pointee.numberArguments()) // 2
闭包
闭包是一个捕获了上下文的常量或变量的函数。
func makeIncrementer() -> () -> Int {
var runningTotal = 10
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
闭包表达式
{(param) -> (returnType) in }
Swift中的闭包即可以当做变量,也可以当做参数传递
var closure : (Int) -> Int = {(age: Int) in return age }
也可以把闭包声明成一个可选类型
var closure : ((Int) -> Int)? closure = nil
可以通过let关键字将闭包声明为一个常量【赋值后就不能修改了】
let closure:(Int) -> Int closure = {(age:Int) in return age }
可以作为函数的参数
func test(param:()->Int){ print(param()) } var age = 10 test { age += 1 return age }
尾随闭包
把闭包表达式作为函数的最后一个参数时,如果当前的闭包表达式很长,那么代码可读性会很差,这个时候可以使用尾随闭包的写法,目的是为了提高代码的可读性。
//定义一个 把【闭包】作为函数的【最后一个参数】的函数
func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{
return by(a, b, c)
}
//未使用尾随闭包的调用
test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
return (item1 + item2 < item3)
})
//使用尾随闭包的调用
test(10, 20, 30) { item1, item2, item3 in
return (item1 + item2 < item3)
}
闭包的优点
- 利用上下文推断参数和返回值类型
- 单表达式可以隐士返回,既省略 return 关键字
- 参数名称的简写(比如我们的 $0)
- 尾随闭包表达式
闭包的本质
var i = 1
let closure = {
print("closure:\(i)")
}
i += 1
print("before closure:\(i)")
closure()
print("after closure:\(i)")
以上的打印结果: 并不会像block一样 在内部创建一个同名变量然后捕获外界变量的值给它
before closure:2
closure:2
after closure:2
编译成看一下SIL代码看一下:闭包内部在访问变量i的时候,并不需要捕获,而是直接到变量i的地址去访问它的值。
还有一种情况,把以上代码放到方法中,在编译成SIL代码看看:
func test(){
var i = 1
let closure = {
print("closure:\(i)")
}
i += 1
print("before closure:\(i)")
closure()
print("after closure:\(i)")
}
test()
闭包存在于方法里面的情况下,就不是直接去访问i的地址了,而是会将i捕获到堆区。
再来个例子:
func makeIncrementer() -> () -> Int {
var runningTotal = 10
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
let makeInc = makeIncrementer()
print(makeInc())
print(makeInc())
print(makeInc())
编译成SIL看一下:
可以看到调用 makeIncrementer(),返回了一个函数,再定位到makeIncrementer()函数对应的SIL:
在它的内部,又调用了内部函数 incrementer(),跳到incrementer()对应的SIL:
在它的内部,对入参
%0执行了project_box指令,赋值给了%1,通过查阅文档可知project_box的作用是,检索box内的值的地址,也就是取到入参%0所在的内存地址。在此之前,入参%0需要进行alloc_box操作,也就是在堆区分配空间。
文档
也就是说,以上incrementer()在使用runningTotal的时候,是将runningTotal捕获到了堆区。
总结:
- swift的闭包在捕获值的过程中,闭包能够捕获了上下文已定义的常量或变量,即使这些常量或者变量的作用于不在了,闭包仍然能够修改它们;
- 当每次修改捕获值得时候,其实是修改堆区的value
- 当每次执行函数的时候,都会重新创建内存空间
闭包的本质
我们需要再往下一层,才能够去探究闭包的本质,需要编译成IR代码。
先熟悉一下IR代码的基本语法
IR语法表达数组:
表达式
[<elementnumber> x <elementtype>]
例子
//24个 i8类型
alloca [24 x i8]
//4个 i32类型
alloca [4 x i32]
i8: 8位整形 or void*
i32: 32位整形
i64: 64位整形
IR语法表达结构体:
表达式
%T = type {<type list>}
例子
//这个结构体有两个成员:%swift.type*、i64
%swift.refcounted = type { %swift.type*, i64 }
IR语法表达指针类型:
表达式
<type>*
例子
//64位整形
i64*
getelementptr指令:
作用:
在LLVM中获取【数组】、【结构体】的成员
语法规则:
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
例子1:
struct munger_struct {
int f1;
int f2;
};
//取得munger_struct的内存地址
getelementptr %struct.munger_struct, %struct.munger_struct * %1, i64 0
//取得f1的内存地址
getelementptr inbounds %struct.munger_struct, %struct.munger_struct * %1, i32 0, i32 0
例子2:
int main(int argc, const char * argv[]) {
int array[4] = {1, 2, 3, 4};
int a = array[0];
return 0;
}
//其中 int a = array[0] 这句对应的LLVM代码应该是这样的:
a = getelementptr inbounds [4 x i32], [4 x i32] * array, i64 0, i32 0
- 总结:
- 第一个索引不会改变返回的指针的类型,也就是说ptrval前面的*对应什么类型,返回就是什么类型
- 第一个索引的偏移量的是由第一个索引的值和第一个ty指定的基本类型共同确定的。
- 后面的索引是在数组或者结构体内进行索引
- 每增加一个索引,就会使得该索引使用的基本类型和返回的指针的类型去掉一层
ok,回到刚才的例子,编译成IR代码后,看到:
这里面有个 s4main15makeIncrementerSiycyF,我们通过xcrun swift-demangle s4main15makeIncrementerSiycyF来看一下:
是在调用makeIncrementer()方法,并且这个函数的返回值:{ i8*, %swift.refcounted* } 是一个结构体,这个结构体里面有一个8位整型的指针、和一个swift.refcounted*类型的指针。
那swift.refcounted*是啥呢,往上翻代码可以找到:
swift.refcounted*也是一个结构体,这个结构体里面有swift.type*这个指针类型,以及一个64位整型,而swift.type*也是一个64位的结构体,也就是说,对于swift.refcounted*来说,我们可以理解为它是一个{i64,i64}的结构体
来个例子:
var i = 10
var closure = {
print("closure:\(i)")
}
print("end")
然后我们看看闭包里面存了什么:
闭包里面存的是一个metadata。
也就是说,闭包也是一个特殊的函数【OC的Block有全局block、栈Block、堆Block,但是在Swift的闭包中没有这些区分】
用Swift还原闭包的结构体
//闭包的本质数据结构 : 闭包的执行地址 + 捕获变量堆空间的地址
struct ClosureData<Box>{
var ptr: UnsafeRawPointer
var object: UnsafePointer<Box>
}
struct HeapObject{
var metadata: UnsafeRawPointer
var refcount1: Int32
var refcount2: Int32
}
struct Box<T>{
var object: HeapObject
var value: T
}
闭包的本质数据结构 : 闭包的执行地址 + 捕获变量堆空间的地址
通过打印来验证:
var f = NoMeanStruct(f: makeIncrementer())
let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)
ptr.initialize(to: f)
let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1){
$0.pointee
}
print(ctx.ptr) //闭包的执行地址 0x0000000100005790
print(ctx.object) //捕获变量堆空间的地址 0x000000010183be90
print("end")
验证1:闭包的执行地址 0x0000000100005790
既然它是闭包函数的地址,那我们就可以在mach-o文件中找到它,在终端使用命令
nm -p mach-o文件 | grep 内存地址(注意内存地址不带0x)
可以得到这个函数地址在mach-o文件中的符号地址,然后我们继续把符号还原一下:
xcrun swift-demangle 符号地址
ok, 到这里就证明了刚才打印的闭包的执行地址确实是正确的。
验证2:捕获变量堆空间的地址 0x000000010183be90