最近在看Swift閉包截獲變量時遇到了各類問題,總結以後發現主要是還用停留在OC時代的思惟來思考Swift問題致使的。藉此機會首先複習一下OC中關於block的細節,同時整理Swift中閉包的相關的問題。不論是目前使用OC仍是Swift,又或者是從OC轉向Swift,均可以閱讀這篇文章並與我交流。html
#OC的blockios
OC的block已經有不少相關的文章介紹了,主要難點在於__block
修飾符的做用和原理,以及循環引用問題。咱們首先由淺入深舉幾個例子看一看__block
修飾符,最後分析循環引用問題。這裏的討論都是基於ARC的。面試
int value = 10;
void(^block)() = ^{
NSLog(@"value = %d", value);
};
value = 20;
block();
// 打印結果是:"value = 10"
複製代碼
OC的block會截獲外部變量,對於int
等基本數據類型,block的內部會拷貝一份,簡單來講,它的實現大概是這樣的:swift
struct block_impl {
//其它內容
int value;
};
複製代碼
由於block內部拷貝了截獲的變量的副本,因此生成block後再修改變量,不會影響被block截獲的變量。同時block內部也不能修改這個變量。閉包
若是要想在block中修改被截獲的基本類型變量,咱們須要把它標記爲__block
:app
__block int value = 10;
void(^block)() = ^{
NSLog(@"value = %d", value);
};
value = 20;
block();
// 打印結果是:"value = 20"
複製代碼
這是由於,對於被標記了__block
的變量,block在截獲它時,會保存一個指針。簡單來講,它的實現大概是這樣的:ide
struct block_impl {
//其它內容
block_ref_value *value;
};
struct block_ref_value {
int value; // 這裏保存的纔是被截獲的value的值。
};
複製代碼
因爲block中一直有一個指針指向value,因此block內部對它的修改,能夠影響到block外部的變量。由於block修改的就是那個外部變量而不是外部變量的副本。優化
上面關於block具體實現的例子只是一個簡化模型,事實上並不是如此,但本質相似。總的來講,只有由__block
修飾符修飾的變量,在被block截獲時纔是可變的。關於這方面的詳細解釋,能夠參考這三篇文章:ui
__block
的實現原理__block
原理的文章,內容會詳細一些。block截獲指針和截獲基本類型是類似的,不過稍稍複雜一些。先看一個最簡單的例子。spa
Person *p = [[Person alloc] initWithName:@"zxy"];
void(^block)() = ^{
NSLog(@"person name = %@", p.name);
};
p.name = @"new name";
block();
// 打印結果是:"person name = new name"
複製代碼
在截獲基本類型時,block內部可能會有int capturedValue = value;
這樣的代碼,類比到指針也是同樣的,block內部也會有這樣的代碼:Person *capturedP = p;
。在ARC下,這實際上是強引用(retain)了block外部的p
。
因爲block內部的p
和外部的p
指向的是同一塊內存地址。因此在block外部修改p
的屬性,依然會影響到block內部截獲的p
。
須要強調一點,這裏的p
依然不是可變的。修改p
的name
不是改變p
,只是改變p
內部的屬性:
Person *p = [[Person alloc] initWithName:@"zxy"];
void(^block)() = ^{
p.name = @"new name"; //OK,沒有改變p
p = [[Person alloc] initWithName:@"new name"]; //編譯錯誤
NSLog(@"person name = %@", p.name);
};
block();
複製代碼
類比__block
修飾符對基本類型的做用原理,由它修飾的指針,在被block截獲時,截獲的實際上是這個指針的指針。好比咱們把剛剛的例子修改一下:
__block Person *p = [[Person alloc] initWithName:@"zxy"];
void(^block)() = ^{
NSLog(@"person name = %@", p.name);
};
p = nil;
block();
// 打印結果是:"person name = (null)"
複製代碼
此時,block內部有一個指向外部的p
的指針,一旦p
被設爲nil
,這個內部的指針就指向了nil
。因此打印結果就是null
了。
還記得之前有一次面試時被問到,__block
會不會retain
變量?答案是:會的。從原理上分析,__block
修飾的變量被封裝在結構體中,block內部持有對這個結構體的強引用。這一點不論是對於基本類型仍是指針都是通用的。從實際例子上來講:
Block block;
if (true) {
__block Person *p = [[Person alloc] initWithName:@"zxy"];
block = ^{
NSLog(@"person name = %@", p.name);
};
}
block();
// 打印結果是:"person name = zxy"
複製代碼
若是沒有retain
被標記爲__block
的指針p
,那麼超出做用於後應該會獲得nil
。
無論對象是否標記爲__block
,一旦block截獲了它,就會強引用它。因此,判斷是否發生循環引用,只要判斷block截獲的對象,是否也持有block便可。若是這個對象確實須要直接或間接持有block,那麼咱們須要避免block強引用這個對象。解決辦法是使用__weak
修飾符。
// block是self的一個屬性
id __weak weakSelf = self;
block = ^{
//使用weakSelf代替self
};
複製代碼
block不會強引用被標記爲__weak
的對象,只會對其產生弱引用。爲了防止在block內的操做會釋放wself
,能夠先強引用它。這種作法有一個很漂亮的名字叫weak-strong dacne
,具體實現方法能夠參考RAC的@strongify
和@weakify
。
簡單來講,除非標記爲__weak
,block老是會強引用任何捕獲的對象。而__block
表示捕獲的就是指針自己,而非另外一個指向這個對象的指針。也就是說,被__block
修飾的對象在block內、外的改動會互相影響。
若是想避免循環引用問題,首先要肯定block引用了哪些對象,而後判斷這些對象是否直接或間接持有block,若是有的話把這些對象標記爲__weak
避免block強引用它。
OC中的__block
是一個很討厭的修飾符。它不只不容易理解,並且在ARC和非ARC的表現大相徑庭。__block
修飾符本質上是經過截獲變量的指針來達到在閉包內修改被截獲的變量的目的。
在Swift中,這叫作截獲變量的引用。閉包默認會截取變量的引用,也就是說全部變量默認狀況下都是加了__block
修飾符的。
var x = 42
let f = {
// [x] in //若是取消註釋,結果是42
print(x)
}
x = 43
f() // 結果是43
複製代碼
若是若是被截獲的變量是引用,和OC同樣,那麼在閉包內部有一個引用的引用:
var block2: (() -> ())?
if true {
var a: A? = A()
block2 = {
print(a?.name)
}
a = A(name: "new name")
}
block2?() //結果是:"Optional("new name")"
複製代碼
若是把變量寫在截獲列表中,那麼block內部會有一個指向對象的強引用,這和在OC中什麼都不寫的效果是同樣的:
var block2: (() -> ())?
if true {
var a: A? = A()
block2 = {
[a] in
print(a?.name)
}
a = A(name: "new name")
}
block2?() //結果是:"Optional("old name")"
複製代碼
Swift會自動持有被截獲的變量的引用,這樣就能夠在block內部直接修改變量。不過在一些特殊狀況下,Swift會作一些優化。經過以前OC中對__block
的分析能夠看到,持有變量的引用確定比直接持有變量開銷更大。因此Swift會自動判斷你是否在閉包中或閉包外改變了變量。若是沒有改變,閉包會直接持有變量,即便你沒有顯式的把它卸載捕獲列表中。下面這句話截取自Swift官方文檔:
As an optimization, Swift may instead capture and store a copy of a value if that value is not mutated by or outside a closure.
不論是否顯示的把變量寫進捕獲列表,閉包都會對對象有強引用。若是閉包是某個對象的屬性,並且閉包中截獲了對象自己,或對象的某個屬性,就會致使循環引用。這和OC中是徹底同樣的。解決方法是在捕獲列表中把被截獲的變量標記爲weak
或unowned
。
關於Swift的循環引用,有一個須要注意的例子:
class A {
var name: String = "A"
var block: (() -> ())?
//其餘方法
}
var a: A? = A()
var block = {
print(a?.name)
}
a?.block = block
a = nil
block()
複製代碼
咱們先建立了可選類型的變量a
,而後建立一個閉包變量,並把它賦值給a
的block
屬性。這個閉包內部又會截獲a
,那這樣是否會致使循環引用呢?
答案是否認的。雖然從表面上看,對象的閉包屬性截獲了對象自己。可是若是你運行上面這段代碼,你會發現對象的deinit
方法確實被調用了,打印結果不是「A」而是「nil」。
這是由於咱們忽略了可選類型這個因素。這裏的a
不是A類型的對象,而是一個可選類型變量,其內部封裝了A的實例對象。閉包截獲的是可選類型變量a
,當你執行a = nil
時,並非釋放了變量a
,而是釋放了a
中包含的A類型實例對象。因此A的deinit
方法會執行,當你調用block時,因爲使用了可選鏈,就會獲得nil
,若是使用強制解封,程序就會崩潰。
若是想要人爲形成循環引用,代碼要這樣寫:
var block: (() -> ())?
if true {
var a = A()
block = {
print(a.name)
}
a.name = "New Name"
}
block!()
複製代碼
爲了不weak
變量在閉包中提早被釋放,咱們須要在block一開始強引用它。這在OC部分已經講過如何使用了。Swift中實現Weak-Strong Dance通常有三種方法。分別是最簡單的if let
可選綁定、標準庫的withExtendedLifetime
方法和自定義的withExtendedLifetime
方法。
__block
修飾符,可是多了截獲列表。經過把截獲的變量標記爲weak
避免引用循環