和Object-C的Block一样,Swift中也有闭包与之对应。
函数类型
函数的类型由函数的返回值类型和参数类型共同组织,而函数类型和函数参数共同确定函数的唯一性。
如果有同名函数,在赋值的时候需要指定类型(很少有人这么写,swift也不推荐这样编码)。这里只是像从侧面证明函数其实也是引用类型。
//函数类型
func func1() {
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
let a: (Int, Int) -> Int = add
let b = a(10, 20)
print(b)
func add(_ a: Double, _ b: Double) -> Double {
return a + b
}
let c: (Double, Double) -> Double = add
let d = c(10.0, 20.0)
print(d)
}
那我们根据上一遍文章的方法,可以从源码中寻找TargetFunctionTypeMetadata,来还原数据结构。
可以得到数据结构:
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 func1() {
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
let a: (Int, Int) -> Int = add
let b = a(10, 20)
print(b)
func add(_ a: Double, _ b: Double) -> Double {
return a + b
}
let c: (Double, Double) -> Double = add
let d = c(10.0, 20.0)
print(d)
print("end")
let cType = type(of: c)
getFunctionInfo(cType as Any.Type)
let aType = type(of: a)
getFunctionInfo(aType as Any.Type)
}
func getFunctionInfo(_ type: Any.Type) {
let funcTypeC = unsafeBitCast(type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
let number = funcTypeC.pointee.numberArguments()
print("函数有\(number)个参数,返回值类型是")
for i in 0..<number {
let argument = funcTypeC.pointee.arguments.index(of: i).pointee
let value = customCast(type: argument)
print(value)
}
}
其中customCast方法如下:
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 {
var container = BrigeProtocolMetadata(type: type, witness: 0)
var protocolType = BrigeProtocol.Type.self
var cast = unsafeBitCast(container, to: BrigeProtocol.Type.self)
return cast
}
得到输出:
类似的方法还可以获取函数的返回值。
认识闭包
闭包是一个可以捕获上下文的常量或者变量的函数!
我们可以先看一看官方给的例子:
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、函数体构成。
闭包可以作为变量、和参数使用。
作为变量:
var closure: (Int) -> Int = { (a: Int) -> Int in
return a + 100
}
作为参数:
func func3(_ someThing: @escaping (() -> Void)) {
print("让子弹飞一会!")
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: someThing)
}
尾随闭包
当闭包表达式作为函数的最后一个参数,如果当前闭包表达式很长,我们可以通过尾随闭包的书写方式来提高代码的可读性。
func func9() {
var age = 10
let a = withUnsafePointer(to: &age) {
$0.pointee
}
let b = withUnsafePointer(to: &age, {ptr in
return ptr.pointee
})
print(a, b)
func8(18, 2) { a1, a2 in
return a1 + a2
}
func8(18, 2, { a1, a2 in
return a2 + a1
})
}
func func8(_ a: Int, _ b: Int, _ complation: (Int, Int) -> Int) {
let a = complation(a, b)
print(a)
}
可以看到尾随闭包让更清晰,但尾随闭包并没有实际意义,只是为了代码美观,增加可读性。
逃逸闭包
当闭包作为一个实际参数传递给一个函数的时候,并且是在函数返回之后调用,我们就说这个闭包逃逸了。当我们声明一个接受闭包作为形式参数的函数时,可以在形式参数前写
@escaping来明确闭包是允许逃逸的。
func func3(_ someThing: @escaping (() -> Void)) {
print("让子弹飞一会!")
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: someThing)
}
class OSDeviceOffLineView: UIView {
var sureClosure: (() -> Void)?
var cancelClosure: (() -> Void) = {}
func youGiveMeTwoClosure(_ sure: @escaping () -> Void, _ cancel: @escaping () -> Void) {
print("set closure")
self.sureClosure = sure
self.cancelClosure = cancel
}
逃逸闭包的特点有:
- 作为函数的参数传递
- 当前闭包在函数内部异步执行或者被存储
- 函数结束,闭包被调用,生命周期结束
如果函数执行完成,闭包也执行完成,这时候就是一个非逃逸闭包,对于非逃逸闭包来说,闭包不需要将函数中的上下文(函数中的变量)捕获到堆区,因为他们的生命周期是一致的,函数执行完成,大家都释放了。但如果是逃逸闭包是需要把捕获变量捕获到堆区的,因为有可能变量已经释放,但闭包还是需要使用。 注意:可选类型默认是逃逸闭包!!!所以总结非逃逸闭包的特点如下:
- 不会产生循环引用,函数作用域内释放
- 编译器更多性能优化 (retain, relsase)
- 上下文的内存保存再栈上,不是堆上
Block
我们知道Object-C中的Block是分为3中类型----GlobelBlock、StackBlock、MallocBlock。
- (void)fun2 {
NSLog(@"%@",^{});
void(^block1)(void) = ^{
};
NSInteger i = 1;
void(^block2)(void) = ^{
NSLog(@"block %ld", i);
};
void(^__weak block3)(void) = ^{
NSLog(@"block %ld", i);
};
NSLog(@"%@",block1);
NSLog(@"%@",block2);
NSLog(@"%@",block3);
}
可以总结,区别大致如下。
| 类别 | 区别 |
|---|---|
| 全局Block | 没有捕获局部变量 |
| 栈Block | 捕获局部变量,在堆区没有强引用 |
| 堆Block | 捕获局部变量,在堆区有强引用 |
| 注意如果有捕获变量,在从栈区引用到堆区的过程中,捕获对象的引用计数会+1.经常会有类似的面试题。 |
- (void)func3 {
NSObject *o = [[NSObject alloc] init];
NSLog(@"CFGetRetainCount print");
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
//1
void(^strongBlock)(void) = ^ {
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
};
strongBlock();
//3 在栈区引用一次,在堆区又引用一次
void(^ __weak weakBlock)(void) = ^{
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
};
weakBlock();
//4栈区引用一次
void(^copyBlock)(void) = [strongBlock copy];
copyBlock();
// 4 本来就在栈上不用+1。
void(^copyBlock1)(void) = [weakBlock copy];
copyBlock1();
// 5
NSLog(@"CFGetRetainCount print");
}
输出:
CFGetRetainCount print**
1**
3**
4**
4**
5**
CFGetRetainCount print**
解析:创建对象赋值给o,引用计数为1,被Block捕获引用计数为2,Block被赋值在堆上的strongBlock,应用计数再次加1,所有第二次打印为3,又被Block捕获,引用计数加1,第三次打印为4,而第一个Block在堆中已经有引用了,所以引用计数不会变,所以第四次打印还是4。而第二个Block在堆中没有引用,再次给它在堆中进行引用后,引用计数再次加1,最后一次打印为5。
闭包是没有这种分类的。
闭包捕获变量
func OSClosureSIL() {
var i: Int = 1
let clousure: ()->Int = {
i = i+1
print(i)
return i
}
let _ = clousure()
}
讲以上代码通过swiftc -emit-sil转换为SIL代码,来看看闭包捕获外部变量的过程。
可以看到是通过
project_box来存放 i的查看官方文档
说白了,做的操作就是在堆区开辟空间来存储它。
为了更好得理解这一过程,我们可以通过结构体来还原闭包的结构,并对它进行内存绑定,来获取闭包捕获的值。
//还原闭包的数据类型
func func5() {
var i: Int = 1
var j: Int = 10
let closure = { () -> Int in
print("closure\(i)")
return i+j
}
struct NoMeanStruct {
var f: () -> Int
}
//转换一下、NoMeanStruct结构和f的结构完全一样。
var f = NoMeanStruct(f: closure)
//新建指针
let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)
//初始化
ptr.initialize(to: f)
//类型转换
let ctx = ptr.withMemoryRebound(to: ClosureData<Box1<Int,Int>>.self, capacity: 1) {
$0.pointee
}
print(ctx.ptr)
print(ctx.object)
print(ctx.object.pointee.value1, ctx.object.pointee.value2)
}
闭包的数据结构:
//闭包的数据结构: 函数地址 + 捕获变量
struct ClosureData<T> {
//函数地址
var ptr: UnsafeRawPointer
var object: UnsafeMutablePointer<T>
}
//捕获1个Int变量
struct Box<T> {
var object: HeapObject
var value: T
}
// 捕获2个Int变量
struct Box1<T1, T2> {
var object: HeapObject
var value1: T1
var value2: T2
}
struct HeapObject{
var metadate: UnsafeRawPointer
//代表1个64位的refCount
var refCount1: UInt32
var refCount2: UInt32
}
输出和调试如下:
可以看到捕获的变量已经找到,还可以通过Mach-o文件来找对应的函数地址,来证实我们的猜想。
我们需要借助
nm -p命令。
$ nm -p <mach-o path> | gerp <函数地址(不带0x)>
注意这里函数地址是不加0x的。
已经找到对应字符串了,继续使用swift-demangle命令来还原字符串。
$ xcrun swift-demangle <string>
可以看到通过字符串也可以对应到函数名称。以上还原过程如果想试,最好用一个mac的Single Line工程尝试,笔者用混编的ios工程没有找对应的函数地址,新的mac工程是可以的。