Swift進(jìn)階08:閉包 & 捕獲原理

閉包

閉包能夠捕獲和存儲(chǔ)定義在其上下文中的任何常量和變量的引用,這也就是所謂的閉合并包裹那些常量和變量客们,因此被稱(chēng)為“閉包”,Swift 能夠?yàn)槟闾幚硭嘘P(guān)于捕獲的內(nèi)存管理的操作材诽。

閉包的三種形式

  • 【全局函數(shù)是一種特殊的閉包】:全局函數(shù)是一個(gè)有名字但不會(huì)捕獲任何值的閉包
//定義一個(gè)全局函數(shù)底挫,只是當(dāng)前的全局函數(shù)并不捕獲值
func test() {
    print("test")
}
  • 【內(nèi)嵌函數(shù)是一個(gè)有名字且能從其上層函數(shù)捕獲值的閉包】:下面的函數(shù)是一個(gè)閉包,函數(shù)中的incrementer是一個(gè)內(nèi)嵌函數(shù)脸侥,可以從makeIncrementer中捕獲變量runningTotal
func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //內(nèi)嵌函數(shù)凄敢,也是一個(gè)閉包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
  • 【閉包表達(dá)式】:是一個(gè)輕量級(jí)語(yǔ)法所寫(xiě)的可以捕獲其上下文中常量或變量值的沒(méi)有名字的閉包

下面是一個(gè)閉包表達(dá)式,即一個(gè)匿名函數(shù)湿痢,而且是從上下文中捕獲變量和常量

//閉包表達(dá)式語(yǔ)法有如下的一般形式:
{ (parameters) -> (return type) in
    statements
}

使用閉包的好處

  • 1涝缝、利用上下文推斷形式參數(shù)和返回值的類(lèi)型;
  • 2譬重、單表達(dá)式的閉包可以隱式返回拒逮,即省略return關(guān)鍵字;
  • 3臀规、簡(jiǎn)寫(xiě)實(shí)際參數(shù)名滩援,例如$0表示第一個(gè)參數(shù);
  • 4塔嬉、尾隨閉包語(yǔ)法玩徊;

閉包表達(dá)式

OC與Swift的對(duì)比

  • OC中的Block其實(shí)是一個(gè)匿名函數(shù),需要具備以下特點(diǎn):

    • 1谨究、作用域 {}
    • 2恩袱、參數(shù)和返回值
    • 3、函數(shù)體(in)之后的代碼
  • Swift中的閉包胶哲,可以當(dāng)做變量畔塔,也可以當(dāng)做參數(shù)傳遞

var clourse: (Int)->(Int) = { (age: Int) in
    return age
}

閉包表達(dá)式的使用方式

  • 【可選類(lèi)型的閉包表達(dá)式】1、將閉包表達(dá)式聲明成一個(gè)可選類(lèi)型
//聲明一個(gè)可選類(lèi)型的閉包
<!--錯(cuò)誤寫(xiě)法-->
var clourse: (Int) -> Int?
clourse = nil

<!--正確寫(xiě)法-->
var clourse: ((Int) -> Int)?
clourse = nil
  • 【閉包常量】2、通過(guò)let將閉包聲明成一個(gè)常量(即一旦賦值之后就不能更改
//2澈吨、通過(guò)let將閉包聲明為一個(gè)常量把敢,即一旦賦值后就不能改變了
let clourse: (Int) -> Int
clourse = {(age: Int) in
    return age
}
//報(bào)錯(cuò):Immutable value 'clourse' may only be initialized once
clourse = {(age: Int) in
    return age
}
image
  • 【閉包參數(shù)】3、將閉包作為 函數(shù)的參數(shù)
//3谅辣、將閉包作為函數(shù)的參數(shù)
func test(param: () -> Int){
    print(param())
}
var age = 10
test { () -> Int in
    age += 1
    return age
}

尾隨閉包

當(dāng)閉包作為函數(shù)的最后一個(gè)參數(shù)修赞,如果當(dāng)前的閉包表達(dá)式很長(zhǎng),我們可以通過(guò)尾隨閉包的書(shū)寫(xiě)方法來(lái)提高代碼的可讀性

//閉包表達(dá)式作為函數(shù)的最后一個(gè)參數(shù)
func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{
    return by(a, b, c)
    
}
//常規(guī)寫(xiě)法
test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
        return (item1 + item2 < item3)
})
//尾隨閉包寫(xiě)法
test(10, 20, 30) { (item1, item2, item3) -> Bool in
    return (item1 + item2 < item3)
}
  • 我們平常使用的array.sorted其實(shí)就是一個(gè)尾隨閉包桑阶,且這個(gè)函數(shù)就只有一個(gè)參數(shù)榔组,如下所示
//array.sorted就是一個(gè)尾隨閉包
var array = [1, 2, 3]
//1、完整寫(xiě)法
array.sorted { (item1: Int, item2: Int) -> Bool in return item1 < item2}
//2联逻、省略參數(shù)類(lèi)型:通過(guò)array中的參數(shù)推斷類(lèi)型
array.sorted { (item1, item2) -> Bool in return item1 < item2}
//3搓扯、省略參數(shù)類(lèi)型 + 返回值類(lèi)型:通過(guò)return推斷返回值類(lèi)型
array.sorted { (item1, item2) in return item1 < item2}
//4、省略參數(shù)類(lèi)型 + 返回值類(lèi)型 + return關(guān)鍵字:?jiǎn)伪磉_(dá)式可以隱式返回表達(dá)包归,即省略return關(guān)鍵字
array.sorted { (item1, item2) in item1 < item2}
//5锨推、參數(shù)名稱(chēng)簡(jiǎn)寫(xiě)
array.sorted {return $0 < $1}
//6、參數(shù)名稱(chēng)簡(jiǎn)寫(xiě) + 省略return關(guān)鍵字
array.sorted {$0 < $1}
//7公壤、最簡(jiǎn):直接傳比較符號(hào)
array.sorted (by: <)

捕獲原理

捕獲一個(gè)值

下面代碼的打印結(jié)果是什么换可?

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    //內(nèi)嵌函數(shù),也是一個(gè)閉包
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = makeIncrementer()
print(makeInc())
print(makeInc())
print(makeInc())

<!--打印結(jié)果-->
11
12
13

打印結(jié)果如下厦幅,從結(jié)果中可以看出沾鳄,每次的結(jié)果都是在上次函數(shù)執(zhí)行的基礎(chǔ)上累加的,但是我們所知的runningTotal是一個(gè)臨時(shí)變量确憨,按理說(shuō)每次進(jìn)入函數(shù)都是10译荞,這里為什么會(huì)每次累加呢? 主要原因:內(nèi)嵌函數(shù)捕獲了runningTotal休弃,不再是單純的一個(gè)變量了

  • 如果是下面這種方式調(diào)用呢吞歼?
print(makeIncrementer()())
print(makeIncrementer()())
print(makeIncrementer()())

<!--打印結(jié)果-->
11
11
11

為什么這種方式每次打印的結(jié)果就是同一個(gè)呢?

1塔猾、SIL分析

將上述代碼通過(guò)SIL分析:

  • 1篙骡、通過(guò)alloc_box在堆上申請(qǐng)了一塊內(nèi)存空間,并將變量存儲(chǔ)到堆上
  • 2丈甸、通過(guò)project_box從堆上取出變量
  • 3糯俗、將取出的變量交給閉包進(jìn)行調(diào)用


    image

結(jié)論:捕獲值的本質(zhì)是 將變量存儲(chǔ)到堆上

2、斷點(diǎn)驗(yàn)證

  • 也可以通過(guò)斷點(diǎn)來(lái)驗(yàn)證睦擂,在makeIncrementer方法內(nèi)部調(diào)用了swift_allocObject方法
image

總結(jié)

  • 一個(gè)閉包能夠從上下文捕獲已經(jīng)定義的常量和變量得湘,即使這些定義的常量和變量的原作用域不存在,閉包仍然能夠在其函數(shù)體內(nèi)引用和修改這些值
  • 當(dāng)每次修改捕獲值時(shí)祈匙,修改的是堆區(qū)中的value值
  • 當(dāng)每次重新執(zhí)行當(dāng)前函數(shù)時(shí)忽刽,都會(huì)重新創(chuàng)建內(nèi)存空間

所以上面的案例中我們知道:

  • makeInc是用于存儲(chǔ)makeIncrementer函數(shù)調(diào)用的全局變量,所以每次都需要依賴(lài)上一次的結(jié)果
  • 而直接調(diào)用函數(shù)時(shí)夺欲,相當(dāng)于每次都新建一個(gè)堆內(nèi)存跪帝,所以每次的結(jié)果都是不關(guān)聯(lián)的,即每次結(jié)果都是一致的

閉包是引用類(lèi)型

這里還要一個(gè)疑問(wèn)些阅,makeInc存儲(chǔ)的到底是什么伞剑?個(gè)人猜測(cè)存儲(chǔ)的是runningTotal的堆區(qū)地址,下面我們通過(guò)分析來(lái)驗(yàn)證是否如此

但是此時(shí)我們發(fā)現(xiàn)市埋,通過(guò)SIL并沒(méi)有辦法分析出什么黎泣,那么可以將SIL降一級(jí),通過(guò)IR代碼來(lái)觀察數(shù)據(jù)的構(gòu)成

在分析之前缤谎,首先來(lái)了解下IR的基本語(yǔ)法

IR基本語(yǔ)法

  • 通過(guò)以下命令將代碼轉(zhuǎn)換為IR文件
swiftc -emit-ir 文件名 > ./main.ll && code main.ll

例如:
- cd 文件所在路徑
- swiftc -emit-ir main.swift > ./main.ll && open main.ll
  • 數(shù)組
/*
- elementnumber 數(shù)組中存放數(shù)據(jù)的數(shù)量
- elementtype 數(shù)組中存放數(shù)據(jù)的類(lèi)型
*/
[<elementnumber> x <elementtype>]

<!--舉例-->
/*
24個(gè)i8都是0
- iN:表示多少位的整型抒倚,即8位的整型 - 1字節(jié)
*/
alloca [24 x i8], align 8
  • 結(jié)構(gòu)體
/*
- T:結(jié)構(gòu)體名稱(chēng)
- <type list> :列表,即結(jié)構(gòu)體的成員列表
*/
//和C語(yǔ)言的結(jié)構(gòu)體類(lèi)似
%T = type {<type list>}


<!--舉例-->
/*
- swift.refcounted:結(jié)構(gòu)體名稱(chēng)
- %swift.type*:swift.type指針類(lèi)型
- i64:64位整型 - 8字節(jié)
*/
%swift.refcounted = type { %swift.type*, i64}
  • 指針類(lèi)型
<type> *

<!--舉例-->
//64位的整型 - 8字節(jié)
i64*
  • getelementptr指令

在LLVM中獲取數(shù)組和結(jié)構(gòu)體的成員時(shí)通過(guò)getelementptr坷澡,語(yǔ)法規(guī)則如下:

<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*

<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*

<!--舉例-->
struct munger_struct{
    int f1;
    int f2;
};
void munge(struct munger_struct *P){
    P[0].f1 = P[1].f1 + P[2].f2;
}

//使用
struct munger_struct* array[3];

int main(int argc, const char * argv[]) {
    
    munge(array);
    
    return 0;
}

通過(guò)下面的命令將c/c++編譯成IR

clang -S -emit-llvm 文件名 > ./main.ll && code main.ll

<!--舉例-->
clang -S -emit-llvm ${SRCROOT}/HTClourseTest/main.c > ./main.ll && "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" main.ll
image

結(jié)合圖來(lái)理解

int main(int argc, const char * argv[]) { 
    int array[4] = {1, 2, 3, 4}; 
    int a = array[0];
    return 0;
}
其中int a = array[0];這句對(duì)應(yīng)的LLVM代碼應(yīng)該是這樣的:
/*
- [4 x i32]* array:數(shù)組首地址
- 第一個(gè)0:相對(duì)于數(shù)組自身的偏移托呕,即偏移0字節(jié) 0 * 4字節(jié)
- 第二個(gè)0:相對(duì)于數(shù)組元素的偏移,即結(jié)構(gòu)體第一個(gè)成員變量 0 * 4字節(jié)
*/
a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i64 0
  • 可以看到其中的第一個(gè)0频敛,使用基本類(lèi)型[4 x i32]项郊,因此返回的指針前進(jìn)0 * 16字節(jié),即當(dāng)前數(shù)組首地址
  • 第二個(gè)index斟赚,使用基本類(lèi)型 i32着降,返回的指針前進(jìn)0字節(jié),即當(dāng)前數(shù)組的第一個(gè)元素拗军,返回的指針類(lèi)型是 i32*
image

總結(jié)

  • 第一個(gè)索引不會(huì)改變返回的指針的類(lèi)型任洞,即ptrval前面的*對(duì)應(yīng)什么類(lèi)型,返回的就是什么類(lèi)型
  • 第一個(gè)索引的偏移量是由第一個(gè)索引的值第一個(gè)ty指定的基本類(lèi)型共同確定的
  • 后面的索引是在數(shù)組或者結(jié)構(gòu)體內(nèi)進(jìn)行索引
  • 每增加一個(gè)索引发侵,就會(huì)使得該索引使用基本類(lèi)型和返回的指針類(lèi)型去掉一層(例如 [4 x i32] 去掉一層是 i32)

IR分析

分析IR代碼
  • 查看makeIncrementer方法
    • 1侈咕、首先通過(guò)swift_allocObject創(chuàng)建swift.refcounted結(jié)構(gòu)體
    • 2、然后將swift.refcounted轉(zhuǎn)換為<{ %swift.refcounted, [8 x i8] }>*結(jié)構(gòu)體(即Box)
    • 3器紧、取出結(jié)構(gòu)體中index等于1的成員變量耀销,存儲(chǔ)到[8 x i8]*連續(xù)的內(nèi)存空間中
    • 4、將內(nèi)嵌函數(shù)的地址存儲(chǔ)到i8即void地址中
    • 5铲汪、最后返回一個(gè)結(jié)構(gòu)體
image

其結(jié)構(gòu)體定義如下


image
仿寫(xiě)

通過(guò)上述的分析熊尉,仿寫(xiě)其內(nèi)部的結(jié)構(gòu)體,然后構(gòu)造一個(gè)函數(shù)的結(jié)構(gòu)體掌腰,將makeInc的地址綁定到結(jié)構(gòu)體中

struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函數(shù)返回值結(jié)構(gòu)體
//BoxType 是一個(gè)泛型狰住,最終是由傳入的Box決定的
struct FunctionData<BoxType>{
    //內(nèi)嵌函數(shù)地址
    var ptr: UnsafeRawPointer
    var captureValue: UnsafePointer<BoxType>
}

//捕獲值的結(jié)構(gòu)體
struct Box<T> {
    var refCounted: HeapObject
    var value: T
}

//封裝閉包的結(jié)構(gòu)體,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: () ->Int
}

//下面代碼的打印結(jié)果是什么齿梁?
func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //內(nèi)嵌函數(shù)催植,也是一個(gè)閉包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = VoidIntFun(f: makeIncrementer())

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內(nèi)存空間
ptr.initialize(to: makeInc)
//將ptr重新綁定內(nèi)存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)

<!--打印結(jié)果-->
0x00000001000018f0
Box<Int>(refCounted: HTClourseTest.HeapObject(type: 0x0000000100004038, refCount1: 2, refCount2: 2), value: 10)
  • 終端命令查找00000001000018f0(其中0x00000001000018f0內(nèi)嵌函數(shù)的地址
nm -p HTClourseTest | grep 00000001000018f0

其中t _$s13HTClourseTest15makeIncrementerSiycyF11incrementerL_SiyFTA是內(nèi)嵌函數(shù)的地址對(duì)應(yīng)的符號(hào)

image

結(jié)論:所以當(dāng)我們var makeInc2 = makeIncrementer()使用時(shí)肮蛹,相當(dāng)于給makeInc2就是FunctionData結(jié)構(gòu)體,其中關(guān)聯(lián)了內(nèi)嵌函數(shù)地址创南,以及捕獲變量的地址伦忠,所以才能在上一個(gè)的基礎(chǔ)上進(jìn)行累加

捕獲兩個(gè)變量的情況

上面的案例中,我們分析了閉包捕獲一個(gè)變量的情況稿辙,如果是將捕獲一個(gè)變量更改為捕獲兩個(gè)變量呢昆码?如下所示修改makeIncrementer函數(shù)

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    //內(nèi)嵌函數(shù),也是一個(gè)閉包
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
  • 查看其IR代碼
image
內(nèi)部結(jié)構(gòu)仿寫(xiě)

根據(jù)捕獲一個(gè)變量的仿寫(xiě)邻储,繼續(xù)仿寫(xiě)捕獲兩個(gè)變量的情況

//2赋咽、閉包捕獲多個(gè)值的原理
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函數(shù)返回值結(jié)構(gòu)體
//BoxType 是一個(gè)泛型,最終是由傳入的Box決定的
struct FunctionData<BoxType>{
    var ptr: UnsafeRawPointer//內(nèi)嵌函數(shù)地址
    var captureValue: UnsafePointer<BoxType>
}

//捕獲值的結(jié)構(gòu)體
struct Box<T> {
    var refCounted: HeapObject
    var value: T
}

//封裝閉包的結(jié)構(gòu)體吨娜,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: () ->Int
}

//下面代碼的打印結(jié)果是什么脓匿?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 0
    //內(nèi)嵌函數(shù),也是一個(gè)閉包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
var makeInc = makeIncrementer(forIncrement: 10)
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內(nèi)存空間
ptr.initialize(to: f)
//將ptr重新綁定內(nèi)存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue)

<!--打印結(jié)果-->
0x00000001000058c0
0x0000000100640310
  • 通過(guò)終端命令查看第一個(gè)地址是否是內(nèi)嵌函數(shù)的地址
image
  • 通過(guò)cat查看 第一個(gè)地址宦赠,即內(nèi)嵌函數(shù)的地址
image
  • x/8g第二個(gè)地址
image
  • 繼續(xù)查看內(nèi)存情況
image

如果將runningTotal改成12呢亦镶?來(lái)驗(yàn)證是否如我們猜想的一樣。事實(shí)證明袱瓮,確實(shí)是存儲(chǔ)的runningTotal

image

所以缤骨,閉包捕獲兩個(gè)變量時(shí),Box結(jié)構(gòu)體內(nèi)部發(fā)生了變化尺借,修改后的仿寫(xiě)代碼如下:

//2屋灌、閉包捕獲多個(gè)值的原理
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函數(shù)返回值結(jié)構(gòu)體
//BoxType 是一個(gè)泛型械蹋,最終是由傳入的Box決定的
struct FunctionData<BoxType>{
    var ptr: UnsafeRawPointer//內(nèi)嵌函數(shù)地址
    var captureValue: UnsafePointer<BoxType>
}

//捕獲值的結(jié)構(gòu)體
struct Box<T> {
    var refCounted: HeapObject
    //valueBox用于存儲(chǔ)Box類(lèi)型
    var valueBox: UnsafeRawPointer
    var value: T
}

//封裝閉包的結(jié)構(gòu)體,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: () ->Int
}

//下面代碼的打印結(jié)果是什么?
func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 12
    //內(nèi)嵌函數(shù)僧鲁,也是一個(gè)閉包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

var makeInc = makeIncrementer(forIncrement: 10)
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內(nèi)存空間
ptr.initialize(to: f)
//將ptr重新綁定內(nèi)存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)
print(ctx.captureValue.pointee.valueBox)

<!--打印結(jié)果-->
0x0000000100005860
Box<Int>(refCounted: HTClourseTest.HeapObject(type: 0x0000000100008098, refCount1: 2, refCount2: 4), valueBox: 0x0000000100481330, value: 10)
0x0000000100481330
疑問(wèn):如果是捕獲3個(gè)變量呢颅停?
  • 如下所示卷谈,是捕獲三個(gè)值的內(nèi)存情況
image
  • 通過(guò)IR文件發(fā)現(xiàn)吸重,從返回值倒推
<!--返回值-->
ret { i8*, %swift.refcounted* } %15

<!--%15-->
%15 = insertvalue { i8*, %swift.refcounted* }
{ i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementer12forIncrement7amount2SiycSi_SitF11incrementerL_SiyFTA" to i8*),
    %swift.refcounted* undef }, %swift.refcounted* %10, 1

<!--%10-->
//與捕獲兩個(gè)變量相比,區(qū)別在于 i64 32 變成了 i64 40
%10 = call noalias %swift.refcounted* @swift_allocObject(
%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata.3, i32 0, i32 2),
i64 40, i64 7) #1

所以Box結(jié)構(gòu)體改為

//捕獲值的結(jié)構(gòu)體
struct Box<T> {
    var refCounted: HeapObject
    //這也是一個(gè)HeapObject
    var valueBox: UnsafeRawPointer
    var value1: T
    var value2: T
}

最終完整的仿寫(xiě)代碼為

//2怪瓶、閉包捕獲多個(gè)值的原理
struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//函數(shù)返回值結(jié)構(gòu)體
//BoxType 是一個(gè)泛型萧落,最終是由傳入的Box決定的
struct FunctionData<BoxType>{
    var ptr: UnsafeRawPointer//內(nèi)嵌函數(shù)地址
    var captureValue: UnsafePointer<BoxType>
}

//捕獲值的結(jié)構(gòu)體
struct Box<T> {
    var refCounted: HeapObject
    //valueBox用于存儲(chǔ)Box類(lèi)型
    var valueBox: UnsafeRawPointer
    var value: T
    var value2: T
}

//封裝閉包的結(jié)構(gòu)體,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: () ->Int
}

//下面代碼的打印結(jié)果是什么洗贰?
func makeIncrementer(forIncrement amount: Int, amount2: Int) -> () -> Int{
    var runningTotal = 12
    //內(nèi)嵌函數(shù)找岖,也是一個(gè)閉包
    func incrementer() -> Int{
        runningTotal += amount
        runningTotal += amount2
        return runningTotal
    }
    return incrementer
}
var makeInc = makeIncrementer(forIncrement: 10, amount2: 18)
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內(nèi)存空間
ptr.initialize(to: f)
//將ptr重新綁定內(nèi)存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
     $0.pointee
}
print(ctx.ptr)
print(ctx.captureValue)
print(ctx.captureValue.pointee)

總結(jié)

  • 1、捕獲值原理:在堆上開(kāi)辟內(nèi)存空間敛滋,并將捕獲的值放到這個(gè)內(nèi)存空間里
  • 2许布、修改捕獲值時(shí):實(shí)質(zhì)是修改堆空間的值
  • 3、閉包是一個(gè)引用類(lèi)型(引用類(lèi)型是地址傳遞)绎晃,閉包的底層結(jié)構(gòu)(是結(jié)構(gòu)體:函數(shù)地址 + 捕獲變量的地址 == 閉包
  • 4蜜唾、函數(shù)也是一個(gè)引用類(lèi)型(本質(zhì)是一個(gè)結(jié)構(gòu)體杂曲,其中只保存了函數(shù)的地址),例如還是以makeIncrementer函數(shù)為例
func makeIncrementer(inc: Int) -> Int{
    var runningTotal = 1
    return runningTotal + inc
}

var makeInc = makeIncrementer

分析其IR代碼袁余,函數(shù)在傳遞過(guò)程中擎勘,傳遞的就是函數(shù)的地址

image

將仿寫(xiě)的FunctionData進(jìn)行修改

struct FunctionData{
    var ptr: UnsafeRawPointer//內(nèi)嵌函數(shù)地址
    var captureValue: UnsafePointer<BoxType>
}

然后改版后的結(jié)構(gòu)仿寫(xiě)如下

//函數(shù)也是引用類(lèi)型
struct FunctionData{
    //函數(shù)地址
    var ptr: UnsafeRawPointer
    var captureValue: UnsafeRawPointer?
}

//封裝閉包的結(jié)構(gòu)體,目的是為了使返回值不受影響
struct VoidIntFun {
    var f: (Int) ->Int
}

func makeIncrementer(inc: Int) -> Int{
    var runningTotal = 1
    return runningTotal + inc
}

var makeInc = makeIncrementer
var f = VoidIntFun(f: makeInc)

let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的內(nèi)存空間
ptr.initialize(to: f)
//將ptr重新綁定內(nèi)存
let ctx = ptr.withMemoryRebound(to: FunctionData.self, capacity: 1) {
     $0.pointee
}

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

<!--打印結(jié)果-->
0x0000000100002140
nil

通過(guò)cat命令查看該地址泌霍,地址就是makeIncrementer函數(shù)的地址

image

總結(jié)

  • 一個(gè)閉包能夠從上下文中捕獲已經(jīng)定義的常量/變量货抄,即使其作用域不存在了述召,閉包仍然能夠在其函數(shù)體內(nèi)引用朱转、修改

    • 1、每次修改捕獲值:本質(zhì)修改的是堆區(qū)中的value值
    • 2积暖、每次重新執(zhí)行當(dāng)前函數(shù)藤为,會(huì)重新創(chuàng)建新的內(nèi)存空間
  • 捕獲值原理:本質(zhì)是在堆區(qū)開(kāi)辟內(nèi)存空間,并將捕獲值存儲(chǔ)到這個(gè)內(nèi)存空間

  • 閉包是一個(gè)引用類(lèi)型(本質(zhì)是函數(shù)地址傳遞)夺刑,底層結(jié)構(gòu)為:閉包 = 函數(shù)地址 + 捕獲變量的地址

  • 函數(shù)也是引用類(lèi)型(本質(zhì)是結(jié)構(gòu)體缅疟,其中保存了函數(shù)的地址)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市遍愿,隨后出現(xiàn)的幾起案子存淫,更是在濱河造成了極大的恐慌,老刑警劉巖沼填,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桅咆,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡坞笙,警方通過(guò)查閱死者的電腦和手機(jī)岩饼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)薛夜,“玉大人籍茧,你說(shuō)我怎么就攤上這事√堇剑” “怎么了寞冯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)晚伙。 經(jīng)常有香客問(wèn)我简十,道長(zhǎng),這世上最難降的妖魔是什么撬腾? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任螟蝙,我火速辦了婚禮,結(jié)果婚禮上民傻,老公的妹妹穿的比我還像新娘胰默。我一直安慰自己场斑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布牵署。 她就那樣靜靜地躺著漏隐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奴迅。 梳的紋絲不亂的頭發(fā)上青责,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音取具,去河邊找鬼脖隶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛暇检,可吹牛的內(nèi)容都是我干的产阱。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼块仆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼构蹬!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起悔据,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤庄敛,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后科汗,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體藻烤,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年肛捍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了隐绵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拙毫,死狀恐怖依许,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缀蹄,我是刑警寧澤峭跳,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站缺前,受9級(jí)特大地震影響蛀醉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜衅码,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一拯刁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逝段,春花似錦垛玻、人聲如沸割捅。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)亿驾。三九已至,卻和暖如春账嚎,著一層夾襖步出監(jiān)牢的瞬間莫瞬,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工郭蕉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疼邀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓恳不,卻偏偏與公主長(zhǎng)得像檩小,于是被迫代替她去往敵國(guó)和親开呐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子烟勋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容