题记:用最通俗的语言,描述最难懂的技术
❝最近在学习和迁移Swift方面的代码,正好看到了闭包这部分,看完之后整个人都被着魔了一样,于是便有了这篇文章,如果有哪些结论模糊或者不准确,请联系
weiniu@sohu-inc.com
Closure是什么
Closure有什么用
使用场景
原理
生成SIL文件
闭包捕获列表
闭包捕获上下文
如何存储捕获值
注意事项
参考文献
结束语
Closure
是Swift
语言下的闭包的实现,就像The Swift Programming Language 5.5 Edition
(链接附文后)中提到的一样,「Closure
是独立的功能块,可以在你的代码中传递和使用」。可以这么理解Closure(swift) ≈ Block(c,c++,objective-c)≈ lambdas (other languages)
同Block
一样,Closure
可以捕获和存储代码上下文中声明的常量和变量。同样Swift
会处理所有捕获的值的内存。
Closure
三种表现形式
有名字的全局闭包,不捕获任何值
有名字的嵌套闭包,从嵌套的方法代码中捕获值
无名字的闭包,作为一个轻量简洁的语法,从上下文中捕获值
Swift
对闭包的优化
自动从上下文推断参数和返回值类型
返回值可以是省略关键字的单行表达式
简短的参数名字
尾随闭包语法
综上所述,它的作用已经很清楚了:可选的传递某些参数从而实现某些回调功能
局部变量
// 通用格式
{ (parameters) -> return type in
statements
}
var variableName: (parametersType) -> return type
eg:
var successClosure: ([String : Int]) -> (Void)
尾随闭包(作为方法的最后一个参数,优化过多的参数和返回值)
func someMethod(closureName: (parameters) -> return type) {...}
eg:
func urlRequest(successBlock: ([String : Int]) -> (Void)) { ... }
逃逸闭包(在方法完成之后进行调用)
func someFuncEscapingClousre(closure: @escaping (parametersType) -> return Type) { ... }
eg:
/// Use @escaping keyword to define
func loadImageCompletion(closure: @escaping () -> Void) { ... }
自动闭包(闭包不带参数,作为函数的参数,返回一个封装的数据作为结果)
func someFuncAutoclosure(closure: () -> return Type) { ... }
eg:
func haveBreakfast(for food: () -> String) { ... }
自动+逃逸
/// @autoclosure @escaping must define
func someFuncAutoEscapeclosure(closure: @autoclosure @escaping () -> return Type) { ... }
eg:
func haveBreakfast(closure: @autoclosure @escaping () -> String) { ... }
实例
或对象
的生命周期的场景❝注释:在Swift中,枚举和结构体的初始化之后应该称为实例,而类初始化之后称之为对象,根据对象的特性,继承特性是区分的关键
如果你对一个问题没有任何思路,那就从相似的问题中找一些突破口,比如Objective-C
下是使用clang
命令把OC
代码转成相对底层的C++
源码,所以我们可以推断,预测有一个xxx
的指令也可以把swift
语言转成相对底层的语言,然后你就搜索相关关键词swift
,底层源码
等去找答案,现在我帮你找好了,这个命令就是swiftc
,目标文件就是SIL(Swift Intermediate Language)
,这个SIL
就等价于OC
中的IR
具体的SIL
相关知识不做讲解,请自行查阅,接下来还是准备工作
创建项目Xcode->File->New->Project
选择macOS->Command Line Tool->Next
填入Product Name
和选择Language
修改为Swift
执行命令,swiftc -emit-sil main.swift | xcrun swift-demangle > ./main.sil
,查看更多使用swiftc -h
❝注释:
xcrun swift-demangle
,是把变量或者方法名混淆还原成可读的代码特别说明下,这个文件可比那个11w的舒服太多了,我们接下来开始分析
在main.swift
中文件写下测试代码,并使用swiftc
命令生成main.sil
中间文件
let bdNum = 3
let printNum = {
[bdNum] in
let _ = bdNum
}
printNum()
查看main.sil
文件
可以清楚的看到闭包在main
函数中被转化为@closure #1
,继续定位该方法的实现
在这个定位过程中,我们发现闭包的类型由() -> ()
变为 (int) -> ()
,猜测应该是把外部的全局变量传入进来了,为了验证猜测我们可以增加几个参数
从这个文件中我们就可以看出,编译器把(Float) -> ()
转化为(Float,Int,String) -> ()
类型,保存捕获列表里的值,类似函数传参一样,进行了值拷贝
let bdNum = 3
let name = "Augus"
let printNum = {
[bdNum,name] (height: Float) in
_ = bdNum
_ = name
}
printNum(1.74)
闭包捕获总结
这个小节以The Swift Programming Language 5.5 Edition
中的例子为例进行分析
func makeIncrementer() -> () -> Int {
var runningTotal = 12
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
main.sil
文件注释
// makeIncrementer()
sil hidden @main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) () -> @owned @callee_guaranteed () -> Int {
bb0:
// 在堆上开辟一个空间,并取名为"runningTotal"
%0 = alloc_box ${ var Int }, var, name "runningTotal" // users: %8, %7, %6, %1
// 把该值和类型包装成project_box的类型
%1 = project_box %0 : ${ var Int }, 0 // user: %4
// 初始化该值为12
%2 = integer_literal $Builtin.Int64, 12 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %4
store %3 to %1 : $*Int // id: %4
// function_ref incrementer #1 () in makeIncrementer()
%5 = function_ref @incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %7
strong_retain %0 : ${ var Int } // id: %6
// 把包装后的"runningTotal"传递给闭包
%7 = partial_apply [callee_guaranteed] %5(%0) : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %9
strong_release %0 : ${ var Int } // id: %8
return %7 : $@callee_guaranteed () -> Int // id: %9
} // end sil function 'main.makeIncrementer() -> () -> Swift.Int'
// incrementer #1 () in makeIncrementer()
sil private @incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) (@guaranteed { var Int }) -> Int {
// %0 "runningTotal" // user: %1
bb0(%0 : ${ var Int }):
// 把传递过来包装后的"runningTotal"值赋值给%1
%1 = project_box %0 : ${ var Int }, 0 // users: %16, %4, %2
debug_value_addr %1 : $*Int, var, name "runningTotal", argno 1 // id: %2
// 要累加的Int类型的值 1
%3 = integer_literal $Builtin.Int64, 1 // user: %8
%4 = begin_access [modify] [dynamic] %1 : $*Int // users: %13, %5, %15
%5 = struct_element_addr %4 : $*Int, #Int._value // user: %6
// 取出"runningTotal"目前的值
%6 = load %5 : $*Builtin.Int64 // user: %8
%7 = integer_literal $Builtin.Int1, -1 // user: %8
// 调用加法
%8 = builtin "sadd_with_overflow_Int64"(%6 : $Builtin.Int64, %3 : $Builtin.Int64, %7 : $Builtin.Int1) : $(Builtin.Int64, Builtin.Int1) // users: %10, %9
%9 = tuple_extract %8 : $(Builtin.Int64, Builtin.Int1), 0 // user: %12
%10 = tuple_extract %8 : $(Builtin.Int64, Builtin.Int1), 1 // user: %11
// 判断是否堆栈溢出
cond_fail %10 : $Builtin.Int1, "arithmetic overflow" // id: %11
// 将计算结果赋值给包装后的"runningTotal"
%12 = struct $Int (%9 : $Builtin.Int64) // user: %13
store %12 to %4 : $*Int // id: %13
%14 = tuple ()
end_access %4 : $*Int // id: %15
// 打开包装,进行最新值的读取
%16 = begin_access [read] [dynamic] %1 : $*Int // users: %17, %18
%17 = load %16 : $*Int // user: %19
end_access %16 : $*Int // id: %18
// 返回最新值
return %17 : $Int // id: %19
} // end sil function 'incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int'
捕获的总结
() -> Int
转化为(@guaranteed { var Int }) -> Int
,引用类型的runningTotal
被当作参数传递进来,从而实现了闭包捕获上下文中变量的过程如果想了解存储的原理,SIL
文件是不够的,这个时候就需要更底层的编译器的指令,还是main.swift
文件,添加以下代码
// 声明一个结构体,添加一个名字为biBao,类型为closure的变量属性
struct BDTest {
var biBao: (() -> ())
}
执行编译器的相关指令swiftc -emit-ir main.swift | xcrun swift-demangle > ./main.ll
%swift.vwtable = type { i8*, i8*, i8*, i8*, i8*, i8*, i8*, i8*, i64, i64, i32, i32 }
%swift.type_metadata_record = type { i32 }
// 1.%swift.type就是 UInt64的封装,所以%swift.type*就是一个指向UInt64整型的指针
%swift.type = type { i64 }
// 2.%swift.refcounted的构成部分,%swift.type*是%swift.type类型的指针,i64为UInt64
%swift.refcounted = type { %swift.type*, i64 }
// 3.BDTest结构体的声明,在llvm下该结构体为<{ %swift.function }>,属性biBao类型为%swift.function
%T4main6BDTestV = type <{ %swift.function }>
// 4.%swift.function的构成部分,i代表Int,后面的数字代表位数,i8=UInt8,i64=UInt64等,%swift.refcounted* 是swift.refcounted类型的指针
%swift.function = type { i8*, %swift.refcounted* }
%"main.BDTest.biBao.modify : () -> () with unmangled suffix ".Frame"" = type {}
%swift.opaque = type opaque
%swift.metadata_response = type { %swift.type*, i64 }
现在最大的疑问就是%swift.refcounted
,所以我们去Swift开源代码(链接附文后)中寻找答案,其余的%swift.function
和%swift.type
均在这个源码文件中找到答案
RefCountedStructTy = llvm::StructType::create(getLLVMContext(), "swift.refcounted");
RefCountedPtrTy = RefCountedStructTy->getPointerTo(/*addrspace*/ 0);
RefCountedNull = llvm::ConstantPointerNull::get(RefCountedPtrTy);
// A type metadata record is the structure pointed to by the canonical
// address point of a type metadata. This is at least one word, and
// potentially more than that, past the start of the actual global
// structure.
TypeMetadataStructTy = createStructType(*this, "swift.type", {
MetadataKindTy // MetadataKind Kind;
});
FunctionPairTy = createStructType(*this, "swift.function", {
FunctionPtrTy,
RefCountedPtrTy,
});
对以上的源码进行分析
我们如果分析RefCountedPtrTy
会比较模糊,但是我们可以根据它的向下一层的结构swift.type
的进行猜测,因为TypeMetadataStructTy
其实就是MetadataKindTy
的封装,而MetadataKindTy
的底层就是HeapObject
,一个基于Objc
的结构,所以目前的闭包用底层结构表达就会类似这样
struct HeapObject {
var Kind: UInt64
var refcount: UInt64
}
struct FunctionPairTy {
// UnsafeMutableRawPointer swift下表示指针的结构体,以后会单独开一篇文章介绍它,在这里你需要知道他是swift下操作内存地址的结构即可
// 闭包代码的实现的内存地址
var FunctionPairTy: UnsafeMutableRawPointer
// 捕获上下文变量的指针,在堆空间,如果没有捕获,为null
var RefCountedStructTy: UnsafeMutablePointer<HeapObject>
}
闭包捕获变量的流程,用llvm
文件进行编译
/// 仍然是官方的例子
func makeIncrementer() -> (() -> Int) {
var runningTotal = 12
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
代码解释
define hidden swiftcc { i8*, %swift.refcounted* } @"main.makeIncrementer() -> () -> Swift.Int"() #0 {
entry:
%runningTotal.debug = alloca %TSi*, align 8
%0 = bitcast %TSi** %runningTotal.debug to i8*
call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)
// 1. %1调用了swift_allocObject向堆申请了空间,类型是%swift.refcounted* 的指针类型
%1 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1
// 2. 把%1类型强转为%2,也就是%2的类型是<{ %swift.refcounted, [8 x i8] }>*的指针类型
%2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
// 3. 重点是%3的结构,%3取的是结构体{ %swift.refcounted, [8 x i8] }类型 %2的第二个元素,也就是 %3是结构体{ %swift.refcounted, [8 x i8] }中 [8 x i8]的指针,分析开始的12放到了该位置
%3 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %2, i32 0, i32 1
// 4. %3 类型强转为 %4
%4 = bitcast [8 x i8]* %3 to %TSi*
store %TSi* %4, %TSi** %runningTotal.debug, align 8
// 5. %._value是取的是%4结构体第一元素的指针
%._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
// 6. 存储UInt64类型的变量12到 %._value,
store i64 12, i64* %._value, align 8
// 7. 引用计数的+1操作
%5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %1) #1
// 8. 引用计数的-1操作
call void @swift_release(%swift.refcounted* %1) #1
// 9. 包装box返回结果
// i8*被插入了 { i8* bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int" to i8*), %swift.refcounted* undef }, 这么长的一段其实就是闭包的实现的内存地址
// %swift.refcounted*则被插入了%1的地址,也就是存放12值的{ %swift.refcounted, [8 x i8] }类型的指针
%6 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int" to i8*), %swift.refcounted* undef }, %swift.refcounted* %1, 1
// 10. 返回包装box结果
ret { i8*, %swift.refcounted* } %6
}
根据以上的代码注释swift
的closure
的底层原理结构可以更具体一些
struct HeapObject {
var Kind: UInt64
var refcount: UInt64
}
// 负责包装的结构体,也就是用来包装捕获需要更新的值
struct Box {
var refCounted: HeapObject
// 这个捕获的值的类型根据捕获的值进行分配,此处规范操作是写泛型
// var value: Int
var value: <T>
}
struct FunctionPairTy {
var FunctionPairTy: UnsafeMutableRawPointer
var RefCountedStructTy: UnsafeMutablePointer<Box>
}
验证猜测
struct FunctionPairTy {
var FunctionPtrTy: UnsafeMutableRawPointer
var RefCountedPtrTy: UnsafeMutablePointer<Box>
}
struct HeapObject {
var Kind: UInt64
var refcount: UInt64
}
struct Box {
var refCounted: HeapObject
var value: Int
}
func makeIncrementer() -> () -> Int {
var runningTotal = 12
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
// 这里需要用结构体把闭包包一层,否则会被底层的逻辑所包装
struct FuncShell {
var fun: () -> Int
}
var fun = FuncShell(fun: makeIncrementer())
var closure = withUnsafeMutablePointer(to: &fun) {
return UnsafeMutableRawPointer($0).assumingMemoryBound(to: FunctionPairTy.self).pointee
}
print(closure)
print("end")
在print("end")
处进行断点,然后进行截图中的一些验证
❝
x/8g 内存地址
:查看内存里的值dis -s 内存地址:查看汇编
捕获值扩展,在main.swift
文件中输入以下代码
func makeIncrementer() -> () -> Int {
var runningTotal = 12
var bd1 = 1
let bd2 = 2
var bd3 = "a"
let bd4 = "b"
func incrementer() -> Int {
runningTotal += 1
bd1 += bd2
bd3 += bd4
return runningTotal
}
return incrementer
}
直接运行swiftc -emit-ir main.swift | xcrun swift-demangle > ./main.ll
命令,直接查看Box
结构体存放的值类型
我们等价替换一下截图中的类型,%swift.refcounted
可以看作是一个包装类型Box
,那么从左到右依次是Box *,Box*,Int,Box*,String
编译器验证,把这段代码替换刚才的那个程序中的同名函数,然后依然是进行断点
通过上述论述不难发现,这和我们的推断是一样的,但是此处还有一个问题,就是所有的值都会被包装Box么?我们依然是通过源码进行定位,把以上的程序生成SIL
文件
可以清楚的看出,凡是变量在闭包内进行更新的就会被包装,反之则不会
引用循环,不管是Block
还是Closure
我们都可以把它当作对象来对待,然后它捕获的变量自然是强引用,如果外部有对该闭包也有一个强引用,那么就会造成引用循环。这也考验我们在实际开发中需要及时对引用关系进行准确的梳理,然后对一方的引用进行弱引用修饰,打破循环即可,原理都是一样的,表现方式不同
Closure
下的引用循环,原理和Block
下的解决思路一致,让我们看看实现方式
class Cat {
let name: String?
lazy var nickName: () -> String = {
// [weak self] in
[unowned self] in
if let name = self.name {
return "nick of \(name)"
} else {
return "none of nick"
}
}
init(name: String?) {
self.name = name
}
deinit {
print("cat is deinitialized")
}
}
// aCat strong to instance of Cat
// instance of Cat strong to () -> String
// () -> String strong to self
var aCat: Cat? = Cat(name: "Tom")
print(aCat!.nickName())
关于选关键词的说明
weak
那么,在以后的代码中使用self
的时候需要加上self?
书写,一方面可读性,另一方面美观都会降低[weak self]
会添加self
的弱引用计数,而弱引用计数需要开辟一个新的空间存SideTable
,SideTable
中会存放弱引用计数及其它引用计数,而开辟空间操作相对于常规操作来说,性能消耗相比unowned
是多的weak
,反之选择unowned
,其实也就是效率的高级体现关于为什么是self?
self
就会被释放,这个时候需要进行安全判断,所以在Objc
下是强化或者非法提前退出进行处理,而在Swift
下则是可选值的使用❝好了,关于iOS下的闭包说了很多,也有一些底层的东西需要去理解和动手,困难肯定是有的,希望一起乘风破浪,所向披靡
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8