swift底层探索 09 - Block捕获外界变量原理swift底层探索 09 - Block捕获外界变量原理

2021-08-09 11:12:48 浏览数 (1)

本文中分析两个问题: 1. Block闭包是一个引用类型 2. Block捕获外部变量

1、Block结构

1.1 IR文件分析

获取IR文件:swiftc -emit-ir 文件地址/main.swift > ./main.ll

代码语言:javascript复制
func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal  = 1
        return runningTotal
    }
    return incrementer
}

IR文件:

  • 可以看到使用swift_allocObject来分配堆内存,间接证明Block是引用类型.
  • 但是不是很直观。
1.2 结构图
1.3 代码结构

一个外部变量:

代码语言:javascript复制
struct FuntionData<T>{
    var ptr: UnsafeRawPointer
    var captureValue: UnsafePointer<Box<T>>
}

struct Box<T> {
    var refCounted: HeapObject
    var value: UnsafePointer<Box<T>>
}

struct HeapObject{
    var type: UnsafeRawPointer
    var refCount: UInt64
}

2、 Block结构仿写

一个外部变量时
代码语言:javascript复制
struct FuntionData<T>{
    var ptr: UnsafeRawPointer
    var captureValue: UnsafePointer<Box<T>>
}

struct Box<T> {
    var refCounted: HeapObject
    var value: UnsafePointer<Box<T>>
}

struct HeapObject{
    var type: UnsafeRawPointer
    var refCount: UInt64
}

//闭包的结构体,方便获取闭包地址
struct VoidIntFun {
    var f: () ->Int
}

func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal  = 1
        return runningTotal
    }
    return incrementer
}
let makeInc = VoidIntFun(f: makeIncrementer())

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: makeInc)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) { $0.pointee }

print(ctx.ptr)
print(ctx.captureValue.pointee)

输出:

  • 不论外部变量是是否发生修改,都将包装成一个Box<T>的结构体
二个外部变量时
代码语言:javascript复制
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 12
    func incrementer() -> Int {
        runningTotal  = amount
        return runningTotal
    }
    return incrementer
}

输出:

  • 如果是两个变量,其中变量二发生了修改(相当于OC中的__block),会包装成对象并存到捕获列表;

如果是这样:

代码语言:javascript复制
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 12
    func incrementer() -> Int {
        return runningTotal   amount
    }
    return incrementer
}

输出:

  • 如果没有发生变化,就直接引用,并不会进行引用类型的包装;

总结

  1. 引用单个变量时,不论当前变量在Block是否发生了变化,都会被包装成对象,存在captureValue捕获列表里
  2. 多个变量时:
    1. 发生变化的外部变量进行对象包装,然后将指针地址存在捕获列表里.
    2. 没有修改的变量就会直接保存变量的值;
  3. 相比之下Swift中的Block捕获方式更加简洁,但是对编译器的要求就会更高;

0 人点赞