iOS下的閉包下篇-Closure
題記:用最通俗的語言,描述最難懂的技術
?最近在學習和遷移Swift方面的代碼,正好看到了閉包這部分,看完之后整個人都被著魔了一樣,于是便有了這篇文章,如果有哪些結論模糊或者不準確,請聯系
weiniu@sohu-inc.com
目錄表
-
Closure是什么
-
Closure有什么用
-
使用場景
-
原理
-
生成SIL文件
-
閉包捕獲列表
-
閉包捕獲上下文
-
如何存儲捕獲值
-
注意事項
-
參考文獻
-
結束語
Closure是什么
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
對閉包的優化
-
自動從上下文推斷參數和返回值類型
-
返回值可以是省略關鍵字的單行表達式
-
簡短的參數名字
-
尾隨閉包語法
Closure有什么用
綜上所述,它的作用已經很清楚了:可選的傳遞某些參數從而實現某些回調功能
語法
局部變量
// 通用格式
{ (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中,枚舉和結構體的初始化之后應該稱為實例,而類初始化之后稱之為對象,根據對象的特性,繼承特性是區分的關鍵
原理
生成SIL文件
如果你對一個問題沒有任何思路,那就從相似的問題中找一些突破口,比如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
是多的 - Apple官方的選擇說明,如果修飾的實例聲明周期短那就選擇
weak
,反之選擇unowned
,其實也就是效率的高級體現
關于為什么是self?
- 如果執行環境是多線程,那么不確定哪個線程會進行調用,調用幾次,代碼塊內完成之后
self
就會被釋放,這個時候需要進行安全判斷,所以在Objc
下是強化或者非法提前退出進行處理,而在Swift
下則是可選值的使用
參考文獻
- The Swift Programming Language 5.5 Edition:https://docs.swift.org/swift-book/
- SIL Doc:https://github.com/apple/swift/blob/main/docs/SIL.rst#abstract
- Swift開源代碼:https://github.com/apple/swift/blob/d5e5253cee0599a5362363d6ba0fe640493ea0e6/lib/IRGen/IRGenModule.cpp#L255
結束語
?好了,關于iOS下的閉包說了很多,也有一些底層的東西需要去理解和動手,困難肯定是有的,希望一起乘風破浪,所向披靡