Swift 六、闭包(上)

函数&闭包.png

函数类型

之前在代码的书写过程中,我们已经或多或少的接触过函数,函数本身也有自己的类型,它由形式参数类型,返回值类型组成。

func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}
var a = addTwoInts
a(10, 20)

addTwoInts就代表了我们当前函数的类型。
那么如果在当前的项目代码当中,出现了相同的函数名称,不过它们的函数参数不同,这个时候我们应该如何去区分哪?

func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}

func addTwoInts(_ a: Double, _ b: Double) -> Double {
    return a + b
}
var a = addTwoInts
image.png

这里两个addTwoInts函数名相同,参数不同,它们是两个不同的函数,在这个过程中,编译器并不能识别addTwoInts到底是哪个函数类型。
所以如果我们想要调用对应的函数方法,需要在变量中指定要赋值的类型。

func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}
func addTwoInts(_ a: Double, _ b: Double) -> Double {
    return a + b
}
var a: (Double, Double) -> Double = addTwoInts
a(10, 20)
var b = a
b(20, 30)

QQ20220126-105152@2x.png

image.png

这里的0x301其实存储的是我们的kind, 所以说我们当前的函数在我们的swift里面也是一个引用类型。
下面我们通过源码来了解一下函数。
metadata.h文件中搜索Function,我们找到了TargetFunctionTypeMetadata,这个就是我们当前的函数类型的源数据。
image.png

我们看到这里有一个TargetFunctionTypeFlags,那么这个类型里究竟都有什么东西哪,我们来看一下。
image.png

了解了当前函数的内部数据结构,我们就可以做一下对应的还原。

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))
        }
    }
}

我们可以通过这个数据结构,可以动态获取函数的相关信息。

func funcInfo() {
    func add(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
    let a: (Int, Int) -> Int = add
    let a1 = a(10, 20)
    print(a1)
    
    func add(_ a: Double, _ b: Double) -> Double {
        return a + b
    }
    
    let b: (Double, Double) -> Double = add
    let b1 = b(10, 20)
    print(b1)
    
    print("end")
    
    let a1Type = type(of: a)
    getFunctionInfo(a1Type as Any.Type)
    
    let b1Type = type(of: b)
    getFunctionInfo(b1Type as Any.Type)
    
}

func getFunctionInfo(_ type: Any.Type) {
    let funcType = unsafeBitCast(type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
    let number = funcType.pointee.numberArguments()
    print("该函数有\(number)个参数,返回值类型是")
    for i in 0..<number {
        let argument = funcType.pointee.arguments.index(of: i).pointee
        let value = customCast(type: argument)
        print(value)
    }
}

protocol BrigeProtocol {}

extension BrigeProtocol {
    static func get(from pointer: UnsafeRawPointer) -> Any {
        pointer.assumingMemoryBound(to: Self.self).pointee
    }
}

struct BrigeProtocolMetadata {
    let type: Any.Type
    let witness: Int
}

func customCast(type: Any.Type) -> BrigeProtocol.Type {
    let container = BrigeProtocolMetadata(type: type, witness: 0)
    let protocolType = BrigeProtocol.Type.self
    let cast = unsafeBitCast(container, to: protocolType)
    return cast
}

funcInfo()

lldb输出打印结果:

30
30.0
end
该函数有2个参数,返回值类型是
Int
Int
该函数有2个参数,返回值类型是
Double
Double

什么是闭包

闭包是一个捕获了上下文的常量或者是变量的函数。
我们可以看一个官方给的案例:

///返回值为函数() -> Int,这个函数() -> Int的返回值是Int
func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

这里incrementer作为一个闭包,显然他是一个函数,其次为了保证其执行,要捕获外部变量runningTotal 到内部,所以闭包的关键就有捕获外部变量或常量函数

闭包表达式

{ (param type) -> (return type) in
    ///do somethings
}

闭包在语法上有这样的标准结构: {(参数列表) -> 返回值 in 闭包体}

  • 作用域(也就是大括号)
  • 参数和返回值
  • 函数体in之后的代码
  1. 闭包即可以当做变量,也可以当做参数传递,这里我们来看一下下面的例子熟悉一下:
var closure: (Int) -> Int = { (age: Int) in
    return age
}
  1. 同样的我们也可以把我们的闭包声明成一个可选类型:
///错误的写法
var closure: (Int) -> Int?
closure = nil

///正确的写法
var closure: ((Int) -> Int)?
closure = nil
  1. 还可以通过 let 关键字将闭包声明为一个常量(也就意味着一旦赋值之后就不能改变了)
let closure: (Int) -> Int
closure = { (age: Int) in
    return age
}

4.同时也可以作为函数的参数

func test(param: () -> Int) {
    print(param())
}

var age = 10
test { () -> Int in
    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) {
}

使用闭包表达式能更简洁的传达信息。当然闭包表达式的好处有很多:

  • 利用上下文推断参数和返回值类型
var array = [1, 2, 3]
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
  • 单表达式可以隐式返回,既省略 return关键字
var array = [1, 2, 3]
array.sort(by: {(item1, item2) in return item1 < item2 })
array.sort{(item1, item2) in item1 < item2 }
  • 参数名称的简写(比如我们的$0)
var array = [1, 2, 3]
array.sort{ return $0 < $1 }
array.sort{ $0 < $1 }
  • 尾随闭包表达式
var array = [1, 2, 3]
array.sort(by: <)

捕获值

在讲闭包捕获值的时候,我们先来回顾一下 OC中Block 捕获值的情形。

- (void)testBlock {
    NSInteger i = 1;
    void(^block) (void) = ^{
        NSLog(@"block %ld:", i);
    };
    i += 1;
    NSLog(@"before block %ld:", i);///2
    block();///1
    NSLog(@"after block %ld:", i);///2
}

那么如果我们想要外部的修改能够影响当前 block 内部捕获的值,我们只需要对当前的 i 添加 __block 修饰符。

- (void)testBlock {
  __block  NSInteger i = 1;
    void(^block) (void) = ^{
        NSLog(@"block %ld:", i);
    };
    i += 1;
    NSLog(@"before block %ld:", i);///2
    block();///2
    NSLog(@"after block %ld:", i);///2
}

那么我们把上面的代码翻译成swift代码来看一下它会发生什么样的变化。

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

那么我们编译成SIL文件来看一下闭包捕获外部变量的过程。

image.png

可以看到是通过project_box来存放 i的查看官方文档

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

image.png

通过上图很明显看出,前8个字节存储的是metadata,在swift当中,其实并没有堆栈全局Block这种区别。

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

let makeInc = makeIncrementer()

print(makeInc())

print(makeInc())

print(makeInc())

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

let makeInc = makeIncrementer()

print(makeIncrementer()())

print(makeIncrementer()())

print(makeIncrementer()())

我们看到,如果我们调用内部的闭包函数,返回的值是累加的,它返回的其实是runningTotal,而下面的代码,我们其实是调用的makeIncrementer,返回的是incrementer。

OC Block 和 Swift闭包相互调用

我们在OC中定义的Block,在Swift中是如何调用的那?我们来看一下。

typedef void(^ResultBlock)(NSError *error);

@interface ZGTest : NSObject

+ (void)testBlockCall:(ResultBlock)block;

@end

@implementation ZGTest

+ (void)testBlockCall:(ResultBlock)block {
    NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:400 userInfo:nil];
    block(error);
}

@end

在 Swift 中我们可以这么使用

ZGTest.testBlockCall { error in
    let errorcast = error as NSError
    print(errorcast)
}

func test(_ block: ResultBlock) {
    let error = NSError.init(domain: NSURLErrorDomain, code: 400, userInfo: nil)
    block(error)
}

同样的,比如我们在 Swift里这么定义,在OC中也是可以使用的。

class ZGTeacher: NSObject {
    @objc static var closure: (() -> ())?
}

在OC中我们可以这么调用

+ (void)test {
    ZGTeacher.closure = ^{
        NSLog(@"end");
    };
}

闭包的本质

  • 闭包的核心是在其中使用的局部变量会被额外地复制或引用,使这些变量脱离其作用域后依然有效。
  • 每次修改捕获值的时候其实是修改堆区当中的value。
  • 当我们每次执行当前函数的时候其实每次都会创建新的内存空间。
    为了更好得理解这一过程,我们可以通过结构体来还原闭包的结构,并对它进行内存绑定,来获取闭包捕获的值。
    为了更清晰探知它内部的调用,我们将代码编译成IR文件,swift代码如下:
func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = makeIncrementer()

swift 还原闭包的结构体

在看具体的内容的时候,我们先来熟悉一下简单的 IR语法

  • 数组
[<elementnumber> x <elementtype>]
//example
alloca [24 x i8], align 8 24个i8都是0
alloca [4 x i32] === array
  • 结构体
%swift.refcounted = type { %swift.type*, i64 }
//表示形式
%T = type {<type list>} //这种和C语言的结构体类似
  • 指针类型
<type> *
//example
i64* //64位的整形
  • getelementptr 指令
    LLVM中我们获取数组和结构体的成员,通过 getelementptr ,语法规则如下:
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
image.png

总结:

  • 第一个索引不会改变返回的指针的类型,也就是说ptrval前面的*对应什么类型,返回就是什么类型。
  • 第一个索引的偏移量的是由第一个索引的值和第一个ty指定的基本类型共同确定的。
  • 后面的索引是在数组或者结构体内进行索引
  • 每增加一个索引,就会使得该索引使用的基本类型和返回的指针的类型去掉一层。
    还原闭包的数据类型
func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

///{ i8*, %swift.refcounted* }
///数据结构:当前闭包的执行地址 + 捕获变量堆空间的地址
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
}

struct NoMeanStruct {
    var f: () -> Int
}

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)
print(ctx.object)
print("end")

输出和调试如下:

QQ20220128-160758@2x.png

可以看到捕获的变量已经找到,还可以通过Mach-o文件来找对应的函数地址,来证实我们的猜想。 这里我们需要借助nm -p命令。

$ nm -p <mach-o path> | grep <函数地址(不带0x)>

注意这里函数地址是不加0x的。


image.png

QQ20220128-161723@2x.png

可以看到通过字符串也可以对应到函数名称。

@convention

@convention :用于修饰函数类型

  • 修饰Swift中的函数类型(调用C函数的时候)
    C文件


    image.png

    image.png

    Swift文件


    image.png
  • 调用OC方法时,修饰Swift函数类型

defer

定义:defer {} 里的代码会在当前代码块返回的时候执行,无论当前代码块是从哪个分支 return 的,即使程序抛出错误,也会执行。
如果多个 defer 语句出现在同一作用域中,则它们出现的顺序与它们执行的顺序相反,也就是先出现的后执行。
defer能做什么?
这里我们看一个简单的例子:

func f() {
    defer {
        print("First defer")
    }
    defer {
        print("Second defer")
    }
    
    defer {
        print("End of function")
    }
}

f()

lldb打印输出结果,验证了先出现的后执行。

End of function
Second defer
First defer

下面案例这里有时候如果当前方法中多次出现 closeFile ,那么我们就可以使用 defer

func append(string: String, terminator: String = "\n", toFileAt url: URL) throws {
    let data = (string + terminator).data(using: .utf8)!
    let fileHandle = try FileHandle(forUpdating: url)
    defer {
        fileHandle.closeFile()
    }
    guard FileManager.default.fileExists(atPath: url.path) else {
        try data.write(to: url)
        return
    }
    
    fileHandle.seekToEndOfFile()
    fileHandle.write(data)
   
}

let url = URL(fileURLWithPath: NSHomeDirectory() + "/Desktop/swift.txt")
try append(string: "iOS面试突击", toFileAt: url)
try append(string: "swift", toFileAt: url)

我们在使用指针的时候

let count = 2
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(repeating: 0, count: count)
defer {
    pointer.deinitialize(count: count)
    pointer.deallocate()
}

比如我们在进行网络请求的时候,可能有不同的分支进行回调函数的执行

func netRquest(completion: () -> Void) {
    defer {
        self.isLoading = false
        completion()
    }
    guard error == nil else { return }
}

defer本质其实是为了管理我们的代码块。

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

推荐阅读更多精彩内容