题记:用最通俗的语言,描述最难懂的技术
❝最近在学习和迁移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下的闭包说了很多,也有一些底层的东西需要去理解和动手,困难肯定是有的,希望一起乘风破浪,所向披靡
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/97Ij2N545ydx6WBNAwncOA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。