7、swift闭包底层探索(上)

128 阅读3分钟

函数类型

函数类型是又 函数参数类型函数返回值类型 构成的

来个例子,以下函数赋值给变量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

Snipaste_2022-06-07_15-13-31.png

所以我们需要给变量指定函数类型,也就是指定 函数参数类型函数返回值类型

var a:(Double, Double) -> Double = addTwoInts

Snipaste_2022-06-07_15-18-01.png

函数是引用类型

变量a存储的函数,本质上是存储了一个metadata,存放在mach-o文件的__DATA_CONST段中。也就是说函数在赋值给变量a的时候,是把函数的metadata地址,赋值给了a,然后从b的第一个八字节看出,里面存放了跟a一样的metadata。所以swift中的函数,也是一个引用类型

Snipaste_2022-06-07_15-36-41.png

探究函数metadata

打开swift源码,到Metadata.h,找到TargetFunctionTypeMetadata,它就是函数的metadata结构体,继承于TargetMetadata。 Snipaste_2022-06-07_15-56-43.png 继承于TargetMetadata里面有kind,也就是isa,TargetFunctionTypeMetadata里面有Flags,看下Flags里面都是什么

通过flags标识了这个函数的类型。

还有一个参数是ResultType,是函数的返回值类型

Snipaste_2022-06-07_16-13-05.png

通过分析,可以将函数的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)
}

闭包的优点

  1. 利用上下文推断参数和返回值类型
  2. 单表达式可以隐士返回,既省略 return 关键字
  3. 参数名称的简写(比如我们的 $0)
  4. 尾随闭包表达式

闭包的本质

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的地址去访问它的值。 Snipaste_2022-06-08_14-52-22.png

还有一种情况,把以上代码放到方法中,在编译成SIL代码看看:

func test(){
    var i = 1
    let closure = {
        print("closure:\(i)")
    }
    i += 1
    print("before closure:\(i)")
    closure()
    print("after closure:\(i)")
}

test()

Snipaste_2022-06-08_16-02-04.png

闭包存在于方法里面的情况下,就不是直接去访问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())

Snipaste_2022-06-09_11-19-02.png

编译成SIL看一下: Snipaste_2022-06-09_14-09-43.png 可以看到调用 makeIncrementer(),返回了一个函数,再定位到makeIncrementer()函数对应的SIL: Snipaste_2022-06-09_14-13-16.png 在它的内部,又调用了内部函数 incrementer(),跳到incrementer()对应的SIL: Snipaste_2022-06-09_14-14-49.png 在它的内部,对入参%0执行了project_box指令,赋值给了%1,通过查阅文档可知project_box的作用是,检索box内的值的地址,也就是取到入参%0所在的内存地址。在此之前,入参%0需要进行alloc_box操作,也就是在堆区分配空间。 文档

也就是说,以上incrementer()在使用runningTotal的时候,是将runningTotal捕获到了堆区。

总结:

  1. swift的闭包在捕获值的过程中,闭包能够捕获了上下文已定义的常量或变量,即使这些常量或者变量的作用于不在了,闭包仍然能够修改它们;
  2. 当每次修改捕获值得时候,其实是修改堆区的value
  3. 当每次执行函数的时候,都会重新创建内存空间

闭包的本质

我们需要再往下一层,才能够去探究闭包的本质,需要编译成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>}*

例子1struct 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


例子2int 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代码后,看到: Snipaste_2022-06-10_11-36-32.png

这里面有个 s4main15makeIncrementerSiycyF,我们通过xcrun swift-demangle s4main15makeIncrementerSiycyF来看一下:

Snipaste_2022-06-10_11-34-29.png

是在调用makeIncrementer()方法,并且这个函数的返回值:{ i8*, %swift.refcounted* } 是一个结构体,这个结构体里面有一个8位整型的指针、和一个swift.refcounted*类型的指针。

swift.refcounted*是啥呢,往上翻代码可以找到: Snipaste_2022-06-10_11-42-57.png

swift.refcounted*也是一个结构体,这个结构体里面有swift.type*这个指针类型,以及一个64位整型,而swift.type*也是一个64位的结构体,也就是说,对于swift.refcounted*来说,我们可以理解为它是一个{i64,i64}的结构体

来个例子:

var i = 10
var closure = {
    print("closure:\(i)")
}
print("end")

然后我们看看闭包里面存了什么:

Snipaste_2022-06-08_15-37-55.png

闭包里面存的是一个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)

Snipaste_2022-06-21_15-19-28.png

可以得到这个函数地址在mach-o文件中的符号地址,然后我们继续把符号还原一下:

xcrun swift-demangle 符号地址

Snipaste_2022-06-21_15-24-06.png

ok, 到这里就证明了刚才打印的闭包的执行地址确实是正确的。

验证2:捕获变量堆空间的地址 0x000000010183be90

Snipaste_2022-06-21_15-29-54.png