weak-strong dance 簡介
使用 Block 時可以通過__weak
來避免循環(huán)引用已經是眾所周知的事情:
// OCClass.m
__weak typeof(self) weakSelf = self;
self.handler = ^{ NSLog(@"Self is %@", weakSelf); };
這時handler
持有 Block 對象肖粮,而 Block 對象雖然捕獲了weakSelf
喇澡,延長了weakSelf
這個局部變量的生命周期度苔,但weakSelf
是附有__weak
修飾符的變量,它并不會持有對象迹鹅,一旦它指向的對象被廢棄了澳腹,它將自動被賦值為nil
嗅战。在多線程情況下,可能weakSelf
指向的對象會在 Block 執(zhí)行前被廢棄净赴,這在上例中無傷大雅绳矩,只會輸出Self is nil
,但在有些情況下(譬如在 Block 中有移除 KVO 的觀察者的邏輯玖翅,在執(zhí)行到該邏輯前 self 就釋放了)就會導致 crash翼馆。這時可以在 Block 內部(第一句)再持有一次weakSelf
指向的對象,保證在執(zhí)行 Block 期間該對象不會被廢棄金度,這就是所謂的 weak-strong dance:
__weak typeof(self) weakSelf = self;
self.handler = ^{
typeof(weakSelf) strongSelf = weakSelf;
// ...
[strongSelf.obserable removeObserver:strongSelf
forKeyPath:kObservableProperty];
};
typeof(weakSelf) strongSelf = weakSelf
這一句等于__strong typeof(weakSelf) strongSelf = weakSelf
应媚,在 ARC 模式下,id 類型和 OC 對象類型默認的所有權修飾符就是__strong
猜极,所以是可以省略的中姜。
問題
上面就是對 weak-strong dance 的掃盲級描述。不知道大家怎么想跟伏,反正我剛聽說這個東西的時候丢胚,是有幾個疑惑的:
-
self
指向的對象已經被廢棄的情況下翩瓜,_handler
成員變量也不存在了,在 ARC 下會自動釋放它指向的 Block 對象携龟,這個時候 Block 對象應該已經沒有被變量所持有了兔跌,它的引用計數應該已經為0了,它應該被廢棄了啊峡蟋,為什么它還能繼續(xù)存在并執(zhí)行坟桅。(這個疑惑其實跟 weak-strong dance 無關,有興趣的可以看看层亿。)比如以下代碼桦卒,在 Block 執(zhí)行前退出這個頁面的話,該 Controller 實例會被廢棄匿又,但 Block 還是會執(zhí)行方灾,會打印“Self is (null)”。
typedef void (^Handler)();
@interface TestViewController ()
@property (nonatomic, strong) Handler handler;
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.handler = ^{
typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"Self is %@", strongSelf);
};
NSTimeInterval interval = 6.0;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)), dispatch_get_main_queue(), weakSelf.handler);
}
- (void)dealloc {
NSLog(@"Released");
}
@end
- 本來在 Block 內部使用
weakSelf
就是為了讓 Block 對象不持有self
指向的對象碌更,那在 Block 內部又把weakSelf
賦給strongSelf
不就又持有self
對象了么裕偿?又循環(huán)引用了?
要解決以上疑惑痛单,需要對 ARC嘿棘、Block、GCD 這些概念有比較深入的了解旭绒,主要是要清楚 Block 的實現原理鸟妙。離職前不久我在公司做過一個關于函數式編程的內部分享,講完 PPT 后有個同學問我“閉包”是怎么實現的挥吵,我當時沒有細說重父,因為不同語言在實現同一個概念時肯定會有一些差異,我也不是什么語言都精通忽匈,所以不敢妄議》课纾現在我也不敢說對所有語言的“閉包”實現都了如指掌,但至少對 OC 的閉包實現——Block 還算心中有數的丹允。下面先簡單介紹一下 Block 的實現郭厌,當然篇幅所限,會略過一些跟今天的主題關系不大的細節(jié)雕蔽。
Block 的實現
Block 是 C 語言的擴展功能折柠,支持 Block 的編譯器會把含有 Block 的代碼轉換成一般的 C 代碼執(zhí)行。之前我一直有用到“Block 對象”這個詞萎羔,因為一個 Block 實例就是一個含有“isa”指針的結構體液走,跟一般的 OC 對象的結構是一樣的:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __xx_block_impl_x {
struct __block_impl impl;
// ...
};
所以跟一般的 OC 對象一樣,這個isa
指針也指向該 Block 實例的類型結構體(類對象,也有叫單件類的)缘眶,Block 有三種類型:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
這三種 Block 類的實例設置在不同的內存區(qū)域嘱根,_NSConcreteStackBlock 的實例設置在 stack 上,_NSConcreteGlobalBlock 的實例設置在 data segment(一般用來放置已初始化的全局變量)巷懈,_NSConcreteMallocBlock 的實例設置在 heap该抒。如果 Block 在記述全局變量的地方被設置或者 Block 沒有捕獲外部變量,那就生成一個 _NSConcreteGlobalBlock 實例顶燕。其它情況都會生成一個 _NSConcreteStackBlock 實例凑保,也就是說,它是在棧上的涌攻,所以一旦它所屬的變量超出了變量作用域欧引,該 Block 就被廢棄了。而當發(fā)生以下任一情況時:
- 手動調用 Block 的實例方法
copy
- Block 作為函數返回值返回
- 將 Block 賦值給附有
__strong
修飾符的成員變量 - 在方法名中含有
usingBlock
的 Cocoa 框架方法或 GCD 的 API 中傳遞 Block
如果此時 Block 在棧上恳谎,那就復制一份到堆上芝此,并將復制得到的 Block 實例的isa
指針設為 _NSConcreteMallocBlock:
imply.isa = &__NSConcreteMallocBlock;
而如果此時 Block 已經在堆上,那就把該 Block 的引用計數加1因痛。
解答疑惑一
說到這里婚苹,已經可以回答上文的第一個疑惑了。把 Block 賦值給self.handler
的時候鸵膏,在棧上生成的 Block 被復制了一份膊升,放到堆上,并被_handler
持有谭企。而之后如果你把這個 Block 當作 GCD 參數使用(比較常見的需要使用 weak-strong dance 的情況)廓译,GCD 函數內部會把該 Block 再 copy 一遍,而此時 Block 已經在堆上债查,則該 Block 的引用計數加1责循。所以此時 Block 的引用計數是大于1的,即使self
對象被廢棄(譬如執(zhí)行了退出當前頁面之類的操作)攀操,Block 會被 release 一次,但它的引用計數仍然大于0秸抚,故而不會被廢棄速和。
捕獲對象變量
Block 捕獲外部變量其實可分為三種情況:
- 捕獲變量的瞬時值
- 捕獲
__block
變量 - 捕獲對象
前兩種情況跟今天的主題關系不大,先按下不表剥汤。第三種情況颠放,也就是本文所舉例子的情況,如果不用__weak
吭敢,而是直接捕獲self
的話碰凶,代碼大概是這個樣子:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __xx_block_impl_y {
struct __block_impl impl;
OCClass *occlass; // 對象型變量不能作為 C 語言結構體成員,可能還需要做一些類型轉換,而且真實生成的代碼并不一定叫 occlass欲低,領會精神……
// ...
};
static void __xx_block_func_y(struct __xx_block_impl_y *__cself) {
OCClass *occlass = __cself -> occlass;
// ...
}
// ...
也就是說辕宏,表示 Block 實例的結構體中會多出一個OCClass
類型的成員變量,它會在結構體初始化時被賦值砾莱。而結構體中的函數指針void *FuncPtr
顯然是用來存放真正的 Block 操作的瑞筐,它會在結構體初始化的時候被賦值為__xx_block_func_y
,__xx_block_func_y
以表示 Block 對象的結構體實例為參數腊瑟,從而得到occlass
這個對象(即被捕獲的self
)聚假。顯然,這里會導致循環(huán)引用闰非,而使用了__weak
之后膘格,表示 Block 對象的結構體中的成員變量occlass
也將附有__weak
修飾符:
__weak OCClass *occlass;
順便說一下,__weak
修飾的變量不會持有對象财松,它用一張 weak 表(類似于引用計數表的散列表)來管理對象和變量瘪贱。賦值的時候它會以賦值對象的地址作為 key,變量的地址為 value游岳,注冊到 weak 表中政敢。一旦該對象被廢棄,就通過對象地址在 weak 表中找到變量的地址胚迫,賦值為 nil喷户,然后將該條記錄從 weak 表中刪除。
那當我們使用 weak-strong dance 的時候是怎么個情況呢访锻,會再次持有對象從而造成循環(huán)引用么褪尝?代碼大致如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __xx_block_impl_y {
struct __block_impl impl;
__weak OCClass *occlass;
// ...
};
static void __xx_block_func_y(struct __xx_block_impl_y *__cself) {
OCClass *occlass = __cself -> occlass;
// ...
}
解答疑惑二
__weak
是個神奇的東西,每次使用__weak
變量的時候期犬,都會取出該變量指向的對象并 retain河哑,然后將該對象注冊到 autoreleasepool 中。通過上述代碼我們可以發(fā)現龟虎,在__xx_block_func_y
中璃谨,局部變量occlass
會持有捕獲的對象,然后對象會被注冊到 autoreleasepool鲤妥。這是延長對象生命周期的關鍵(保證在執(zhí)行 Block 期間對象不會被廢棄)佳吞,但這不會造成循環(huán)引用,當函數執(zhí)行結束棉安,變量occlass
超出作用域底扳,過一會兒(一般一次 RunLoop 之后),對象就被釋放了贡耽。所以 weak-strong dance 的行為非常符合預期:延長捕獲對象的生命周期衷模,一旦 Block 執(zhí)行完鹊汛,對象被釋放,而 Block 也會被釋放(如果被 GCD 之類的 API copy 過一次增加了引用計數阱冶,那最終也會被 GCD 釋放)刁憋。
額外好處
上文說了每使用一次_weak
變量就會把對象注冊到 autoreleasepool 中,所以如果短時間內大量使用_weak
變量的話熙揍,會導致注冊到 autoreleasepool 中的對象大量增加职祷,占用一定內存。而 weak-strong dance 恰好無意中解決了這個隱患届囚,在執(zhí)行 Block 時有梆,把_weak
變量(weakSelf)賦值給一個臨時變量(strongSelf),之后一直都使用這個臨時變量意系,所以_weak
變量只使用了一次泥耀,也就只有一個對象注冊到 autoreleasepool 中。