前情提要
Swift的閉包和OC的Block是一回事,是一種特殊的函數-帶有自動變量的匿名函數。
分別從語法和原理上講解閉包
語法
Swift中,函數和閉包都是引用類型
無論你什么時候賦值一個函數或者閉包給常量或者變量,你實際上都是將常量和變量設置為對函數和閉包的引用
如果你分配了一個閉包給類型的屬性,并且閉包通過引用該實例或者它的成員來捕獲實例,你將在閉包和實例間產生循環引用
閉包表達式:
閉包表達式語法能夠使用常量形式參數,變量形式參數,輸入輸出形式參數,以及可變形式參數,但在使用可變形式參數時需要在形式參數列表的最后面使用。參數不能提供默認值。元組也可被用來作為形式參數和返回類型
{? ? (parameter) -> (return type) in
? ? statements
}
從sorted(by:)函數說起,它會根據你提供的排序閉包將已知類型的數組的值進行排序。一旦完成,sorted方法會返回排序好的新數組并且不會改變原數組。
不用閉包:
let names = ["zhangsan", "lisi", "wang2", "liu5"]
func backward(_ s1: String, _ s2: String) -> Bool {
? ? return s1 > s2
}
var reversedNames = names.sorted(by: backward)
print(reversedNames)
使用閉包:
let names = ["zhangsan", "lisi", "wang2", "liu5"]
let?reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in?
? ? return s1 > s2
?})
print(reversedNames)
因為排序閉包作為實際參數來傳遞給函數,所以Swift能推斷出它的形式參數類型和返回值類型
sorted(by:)方法期望它的形式參數是一個(String, String) -> Bool類型的函數。這意味著(String, String)和Bool類型不需要被寫成閉包表達式定義中的一部分,因為所有的類型都能被推斷,返回箭頭(->)和圍繞在形式參數名周圍的括號也能被省略。
let names = ["zhangsan", "lisi", "wang2", "liu5"]
let?reversedNames = names.sorted(by: { s1, s2 in return s1 > s2?})
print(reversedNames)
單表達式的閉包能夠通過從它們的聲明中刪掉return關鍵字來隱式返回它們單個表達式的結果(函數也可以,重點是單表達式)
let names = ["zhangsan", "lisi", "wang2", "liu5"]
let?reversedNames = names.sorted(by: { s1, s2 in s1 > s2?})
print(reversedNames)
Swift自動對行內閉包提供簡寫實際參數名,可以通過$0,$1,$2等名字來引用閉包的實際參數值。
let names = ["zhangsan", "lisi", "wang2", "liu5"]
let?reversedNames = names.sorted(by: {? $0 > $1?})
print(reversedNames)
Swift的String類型定義了關于大于號(>)的特定字符串實現,讓其作為一個有兩個String類型形式參數的函數并返回一個Bool類型的值。這正好與sorted(by:)方法相匹配。因此,你能簡單地傳遞一個大于號,并且Swift將推斷你想使用大于號特殊字符串函數實現。
let names = ["zhangsan", "lisi", "wang2", "liu5"]
let?reversedNames = names.sorted(by: >?)
print(reversedNames)
尾隨閉包
如果你需要將一個很長的閉包表達式作為函數最后一個實際參數傳遞給函數,使用尾隨閉包將增強函數的可讀性。尾隨閉包是一個被書寫在函數形式參數的括號外面的閉包表達式,例:XXX(_ xx: X, ...)尾隨閉包
let names = ["zhangsan", "lisi", "wang2", "liu5"]
let?reversedNames = names.sorted{? $0 > $1?}
print(reversedNames)
捕獲值
一個閉包能夠從上下文捕獲已被定義的常量和變量。即使定義這些常量和變量的原作用于已經不存在,閉包仍能夠在其函數體內引用和修改這些值
作為一種優化,如果一個值沒有改變或者在閉包的外面(閉包沒用它),Swift不會去捕獲它
Swift也處理了變量的內存管理操作,當變量不再需要時會被釋放
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
? ? var runningTotal = 0
? ? func incrementer() -> Int {
// 捕獲了runnigTotal 并在這個閉包(內嵌函數)內,申請了一塊新的內存空間給變量runningTotal
? ? ? ? runningTotal += amount
? ? ? ? retun runningTotal
????}
? ? return incrementer
}
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen() --10
incrementByTen() --20
incrementByTen() --30
如果你有一個新的變量引用了函數makeIncrementer
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen() --10
incrementByTen() --20
incrementByTen() --30
let incrementBySeven =makeIncrementer(forIncrement:7)
let incrementBySeven() --7
let incrementBySeven() --14
let incrementBySeven() --21
通過內存地址來分析這一過程都發生了什么?
分析代碼:
fileprivate?func?makeIncrementer(forIncrement amount:Int) -> () ->Int{
? ? ? ? var?runningTotal =0
? ? ? ? print("-------------------")
? ? ? ? print("內嵌函數外的runningTotal地址:")
? ? ? ? print(Unmanaged.passUnretained(runningTotalasAnyObject).toOpaque())
? ? ? ? print("-------------------")
? ? ? ? func?incrementer() ->Int{
? ? // 捕獲了runnigTotal 并在這個閉包(內嵌函數)內,申請了一塊新的內存空間給變量runningTotal
? ? ? ? ? ? runningTotal+=amount
? ? ? ? ? ? print("內嵌函數內的runningTotal地址:")
? ? ? ? ? ? print(Unmanaged.passUnretained(runningTotalasAnyObject).toOpaque())
? ? ? ? ? ? return?runningTotal
? ? ? ? }
? ? ? ? return incrementer
? ? }
? ? ? ?leti ncrementByTen =makeIncrementer(forIncrement:10)
? ? ? ? print(incrementByTen())
? ? ? ? print(incrementByTen())
? ? ? ? print(incrementByTen())
? ? ? ? let?incrementBySeven =makeIncrementer(forIncrement:7)
? ? ? ? print(incrementBySeven())
? ? ? ? print(incrementBySeven())
? ? ? ? print(incrementBySeven())
? ? ? ? print(makeIncrementer(forIncrement:10)())
? ? ? ? print(makeIncrementer(forIncrement:10)())
? ? ? ? print(makeIncrementer(forIncrement:11)())
代碼對應的print:
-------------------
內嵌函數外的runningTotal地址:
0xbd27871c8d0870d3
-------------------
內嵌函數內的runningTotal地址:
0xbd27871c8d08707310
內嵌函數內的runningTotal地址:
0xbd27871c8d08719320
內嵌函數內的runningTotal地址:
0xbd27871c8d08713330
-------------------
內嵌函數外的runningTotal地址:
0xbd27871c8d0870d3
-------------------
內嵌函數內的runningTotal地址:
0xbd27871c8d0870a37
內嵌函數內的runningTotal地址:
0xbd27871c8d08703314
內嵌函數內的runningTotal地址:
0xbd27871c8d08718321
-------------------
內嵌函數外的runningTotal地址:
0xbd27871c8d0870d3
-------------------
內嵌函數內的runningTotal地址:
0xbd27871c8d08707310
-------------------
內嵌函數外的runningTotal地址:
0xbd27871c8d0870d3
-------------------
內嵌函數內的runningTotal地址:
0xbd27871c8d08707310
-------------------
內嵌函數外的runningTotal地址:
0xbd27871c8d0870d3
-------------------
內嵌函數內的runningTotal地址:
0xbd27871c8d08706311
分析
1.變量incrementByTen和incrementBySeven引用的函數makeIncrementer是相同的,makeIncrementer(forIncrement:10)和makeIncrementer(forIncrement:7)所返回的內嵌函數(閉包)地址也是相同的,并沒有創建兩個blcok;
2.在執行賦值(let?incrementByTen = makeIncrementer(forIncrement:10))的過程中,內嵌函數外的runningTotal地址也是相同的;
3.在執行incrementByTen()時,內嵌函數內的runningTotal地址每一次都是不同的,意味著runningtotal被blcok所捕獲,每執行一次都會在前一次的基礎上把runningtotal復制到一個新地址上再執行相加。
4.累加,是因為變量incrementByTen/incrementBySeven維護著自己block捕獲的變量。當單獨執行makeIncrementer(forIncrement:10)()時,無論多少次,也不會累加,因為沒有變量去維護block捕獲的變量,執行完就釋放了。
5.第一次執行incrementByTen()和直接執行makeIncrementer(forIncrement:10)(),的內嵌函數runningTotal地址一樣,這是內存行為,提高了效率。
逃逸閉包
當閉包作為一個實際參數傳遞給一個函數的時候,并且它會在函數返回之后調用(比如封裝的網絡請求),我們就說這個閉包逃逸了。當你聲明一個接受閉包作為形式參數的函數時(比如封裝的帶閉包的網絡請求),你可以在形式參數前寫@escaping來明確閉包是允許逃逸的。
閉包可以逃逸的一種方法是被存儲在定義于函數外的變量里。比如說,很多函數接收閉包實際參數來作為啟動異步任務的回調。函數在啟動任務后返回,但是閉包要直到任務完成--閉包需要逃逸,以便稍后調用
讓閉包@escaping意味著你必須在閉包中顯示地引用self
var completionHandlers: [() -> void] = []
func someFunctionWithEscapingClousure(completionHandler: @escaping () -> void) {
? ? completionHandlers.append(completionHandler)
}
func someFuncWithNonEscapingClosure(closure: () -> void) {
? ? closure()
}
class SomeClass {
? ? var x = 10
? ? func doSomething() {
????????someFunctionWithEscapingClousure {self.x = 100}
? ? ? ? ?someFuncWithNonEscapingClosure {x = 200}
????}
}
let instance = someClass()
instance.doSomething()
print(instance.x) -- 200
completionHandlers.first?()
print(instance.x) -- 100
自動閉包
自動閉包是一種自動創建的用來把作為實際參數傳遞給函數的表達式打包的閉包。它不接受任何實際參數,并且當它被調用時,它會返回內部打包的表達式的值。
這個語法的好處在于通過寫普通表達式代替顯示閉包而使你省略包圍函數形式參數的括號
public func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticsString = #file, line: Uint = #line)
let number = 3
assert(number > 3, "number 不大于3")
自動閉包允許你延遲處理,因此閉包內部的代碼直到你調用它的時候才會運行。對于有副作用或者占用資源的代碼來說很有用,因為它可以允許你控制代碼何時才進行求值
var customersInLine = ["熬", "2", "3", "4", "5"]
print(customersInLine.count) -- 5
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count) -- 5
print("Now serving \(customerProvider())!") -- 熬
print(customersInLine.count) -- 4
當你傳一個閉包作為實際參數到函數的時候,你會得到與延遲處理相同的行為
fileprivate?func?serve(customer customerProvider: () ->String) {
? ? ? ? print("Now serving \(customerProvider())!")
? ? }
serve(customer: {customersInLine.remove(at: 0)}) --?Now serving 熬!
通過@autoclosure標志標記他的形式參數使用了自動閉包。現在你可以調用函數就像它接收了一個String實際參數而不是閉包。實際參數自動地轉換為閉包,因為customerProvider形式參數的類型被標記為@autoclosure。
fileprivate?func?serve(customer customerProvider: @autoclosure () ->String) {
? ? ? ? print("Now serving \(customerProvider())!")
? ? }
serve(customer: customersInLine.remove(at: 0)) --Now serving 熬!
同時使用逃逸閉包和自動閉包
? ??????func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) ????????????{
? ? ? ????????? customerProvides.append(customerProvider)
? ????????? }
? ??????var customerProvides: [() -> String] = []
? ??????var?customersInLine = ["熬","2","3","4","5"]
? ? ? ? collectCustomerProviders(customersInLine.remove(at:0))
? ? ? ? collectCustomerProviders(customersInLine.remove(at:0))
? ? ? ? print("Collected\(customerProvides.count)closures.")
? ? ? ? for?customerProvider?in?customerProvides{
? ? ? ? ? ? print("Now serving \(customerProvider())!")
? ? ? ? }
? ? ? ? print("Remainning\(customerProvides.count)closures.")
原理
閉包/Block是一個,帶有自動變量值(可以截獲自動變量值)的匿名函數;
截獲的含義是保存改自動變量的瞬間值
OC中如果要改變block截獲的外部自動變量的值需要在改值前加上__block修飾符,Swift不用,系統會自動處理這些問題。延伸到對象,對于Swift來說系統也會處理但OC不同。
-OC下,截獲一個對象(NSMutableArray)mArray,在block內調用NSMutableArray的方法(addObject:)沒問題,因為Block捕獲的是NSMutableArray類對象用的結構體實例指針,但對其進行操作,比如賦值,就會產生編譯錯誤,要在變量前加上__block。Swift下直接用就行了。
從C代碼的角度分析closure/block。OC可以直接用 clang -rewrite-objc 原文件名 來分析,swift下的clang我沒有找到對應的命令,所以只看oc的。在分析之前,先介紹兩個小概念。
1.OC&Swift中的self
OC Code:
- (void) method:(int)arg {
? ? NSLog(@"%p %d\n", self, arg);
}
MyObject *obj = [[MyObject alloc] init];
[obj method: 10];
轉換成C Code:
void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg) {
? ??NSLog(@"%p %d\n", self, arg);
}
MyObject *obj = objc_msgSend(objc_getClass("MyObject"), sel_registerName("alloc"));
obj = objc_msgSend(obj, sel_registerName("init"));
objc_msgSend(obj, sel_registerName("method:"), 10);
最后一條執行語句,也就是[obj method: 10] ->?objc_msgSend(obj, sel_registerName("method:"), 10)。objc_msgSend(obj, sel_registerName("method:"), 10)函數根據指定的對象和函數名,從對象持有類(MyObject)的結構體中檢索method:10對應的函數void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)的指針并調用。此時,objc_msgSend函數的第一個參數obj作為_I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)的第一個參數self進行傳遞。即self就是Myobject類的對象-obj自己。
2.OC中的id
id在C語言中的聲明:
typedef struct objc_object {
? ? Class isa;
} *id;
-id為objc_object結構體的指針類型。
Class在C語言中的聲明:
typedef struct objc_class {
????Class isa;
} *Class;
-Class為objc_class結構體的指針類型。
這與objc_object結構體相同。然而,objc_object結構體和objc_class結構體歸根結底是在各個對象(id)和類(Class)的實現中使用的最基本的結構體。
例:
類MyObject
@interface MyObject : NSObject {
? ? int val0;
? ? int val1;
}
@end
他是結構體,一個基于objc_class結構體的class_t結構體。
struct class_t {
? ? struct class_t *isa;
? ? struct class_t *superclass;
? ? Cache cache;
? ? IMP *vtable;
? ? uintptr_t data_NEVER_USE;
};
而基于MyObject創建的對象(MyObject1/2/3/4/5)的結構體:
struct MyObject1/2/3/4/5 {
? ? Class isa;
? ? int val0;
? ? int val1;
}
MyObject類的實例變量val0,val1被直接聲明為對象的結構體成員。OC中由類生成對象就意味著,生成一個基于該類的結構體實例,這些結構體實例(對象)通過isa保持該類的結構體實例指針(這個例子就是*class_t)。舉個例子就是:有3個類,C動物->C狗->C柯基,基于柯基生成的一個對象叫做KK,那么KK這個結構體就會有一個isa指向C柯基,而這個C柯基類的結構體中還會有一個isa指向C狗,同理C狗的isa指向C動物,一層一層地指向父類(super),直到Class,而Class中的isa指向的就是自己,也就到頭了。
class_t中持有,聲明的成員變量,方法的名稱,方法的實現(函數指針),屬性以及父類的指針,并被OC運行時庫所使用的。
介紹完兩個小概念,開始從C代碼的角度分析Block:
.m文件下的代碼
int?main() {
? ? void(^blk)(void) = ^{printf("Block\n");};
? ? blk();
? ? return0;
}
clang -rewrite-objc 項目文件.m之后,摘取有用的內容:
```c
struct?__block_impl {
? void*isa;
? intFlags;
? intReserved;
? void*FuncPtr;
};
struct?__main_block_impl_0 {
? struct?__block_impl impl;
? struct?__main_block_desc_0* Desc;
? __main_block_impl_0(void?*fp,?struct?__main_block_desc_0 *desc,?int?flags=0) {
? ? impl.isa = &_NSConcreteStackBlock;
? ? impl.Flags = flags;
? ? impl.FuncPtr = fp;
? ? Desc = desc;
? }
};
static?void__main_block_func_0(struct__main_block_impl_0 *__cself) {printf("Block\n");}
static?struct__main_block_desc_0 {
? size_t reserved;
? size_t Block_size;
}
__main_block_desc_0_DATA= {0,?sizeof(struct?__main_block_impl_0)};
int?main() {
? ? void(*blk)(void) = ((void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA));
? ? ((void(*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
? ? return0;
}
```
粗略來看大概有5部分,一步一步看;
1.源碼中的^{printf("Block\n");};轉換后的代碼是static?void__main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");}
通過轉換后的代碼可知,通過Block使用的匿名函數實際上被作為簡單的C語言函數來處理。
另外根據Block語法所屬的函數名(此處為main)和該Block語法在該函數出現的順序值(此處為0)來給經clang變換的函數命名(void__main_block_func_0)。
該函數的參數*__cself就是指向Block值的變量,就是他自己。相當于OC實例方法中指向對象自身的變量self。上面的第一個小概念有詳細解釋。來看看該參數,__cself是__main_block_impl_0結構體的指針。
那我們來看看__main_block_impl_0結構體,去掉構造函數后:
struct?__main_block_impl_0 {
? struct?__block_impl impl;
? struct?__main_block_desc_0* Desc;
};
第一個成員變量是impl,他的結構體聲明是:
struct__block_impl {
? void?*isa;
? int?Flags;
? int?Reserved;
? void?*FuncPtr;
};
impl是虛函數列表,該結構體內存放了一個標志,包括今后版本升級所需的區域及函數指針。
第二個成員變量是Desc指針,他的結構體的聲明是:
static?struct__main_block_desc_0 {
? size_t reserved;
? size_t Block_size;
}__main_block_desc_0_DATA= {0,sizeof(struct__main_block_impl_0)};
這些也如同其成員名稱所示,其結構為今后版本升級所需的區域和Block大小。
這兩個成員變量看完之后,再來看一下初始化含有這些結構體的__main_block_impl_0結構體的構造函數,就是我們剛才忽略的那一部分代碼,這個代碼執行的完成意味著_cself初始化完成:
__main_block_impl_0(void?*fp,struct__main_block_desc_0 *desc,intflags=0) {
? ? impl.isa = &_NSConcreteStackBlock;
? ? impl.Flags = flags;
? ? impl.FuncPtr = fp;
? ? Desc = desc;
? }
在分析這個結構體之前,先看一下這個構造函數的調用(在 int main() {...}中):
void(*blk)(void) = ((void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA));
去掉轉換部分 簡化成兩行代碼:
struct __main_block_impl_0 tmp =?__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct?__main_block_impl_0 *blk = &tmp;
該源代碼將__main_block_impl_0結構體類型的自動變量(棧上生成的__main_block_impl_0結構體實例的指針),賦值給__main_block_impl_0結構體指針類型的變量blk,對應未轉換前的代碼就是這個賦值操作:
void(^blk)(void) = ^{printf("Block\n");};
即將Block語法生成的Block賦給Block類型變量blk。該源代碼中的Block就是__main_block_impl_0結構體類型的自動變量,即棧上生成的__main_block_impl_0結構體實例。然后指針賦值。
接下來看看__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);結構體實例構造參數。__main_block_func_0和&__main_block_desc_0_DATA。
__main_block_func_0是一個由Block語法轉換的C語言函數指針;
&__main_block_desc_0_DATA是作為靜態全局變量初始化的__main_block_desc_0結構體實例指針。其初始化代碼如下:
__main_block_desc_0_DATA= {0,sizeof(struct__main_block_impl_0)};
由此可知,改源代碼使用Block,即__main_block_impl_0結構體實例的大小,進行初始化。
下面綜合來看一下,棧上的Block即__main_block_impl_0結構體實例的初始化是怎樣完成的(展開):
struct__main_block_impl_0 {
? ? void *isa;
? ? Int Flags;
? ? int Reserved;
? ? void &FuncPtr;
? ? struct?__main_block_desc_0* Desc;
}
該結構體會根據構造函數進行如下的初始化:
isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr =?__main_block_func_0;
Desc = &__main_block_desc_0_DATA;
即,將__main_block_func_0函數指針賦值給成員變量FunPtr。
源碼中使用blk的部分blk()對應轉換后的代碼是:
((void(*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
去掉轉換部分:
(*blk->impl.FuncPtr)(blk);
這就是簡單地使用函數指針調用函數。正如我們剛才所確認的,有Block語法轉換的__main_block_func_0函數的指針被賦值成員變量FuncPtr中。另外也說明了,__main_block_func_0函數的參數__cself指向Block值。在調用改函數的源代碼中可以看出Block正是作為參數進行了傳遞。