http://pandara.xyz/2016/04/02/weakify_strongify/
什么是循環(huán)引用
先說循環(huán)引用,這個東西大概是大多數(shù)程序猿都應(yīng)該知道的常識希俩,不僅是 Objective-C 泣矛,很多其他語言也有這個概念。假設(shè)有兩個類 A 與 B毅臊,A 中存在一個 ivar objB榕吼,B 中存在一個 ivar objA。現(xiàn)在各有一個 A 與 B 類型的對象: a 與 b勉失,如果:
a.objB = b;
b.objA = a;
這樣就會產(chǎn)生循環(huán)引用羹蚣,如下圖:
image
iOS 中 ARC 內(nèi)存管理機制用一句話概括起來就是:
如果一個對象對象沒有被其他對象強引用,那么它就將被銷毀
而由于 a 與 b 彼此強引用了對方乱凿,他們兩者都無法被釋放顽素。要解決這個問題也簡單咽弦,打破某一條強引用,將它變成弱引用就是了胁出,比如可以:
@interface A
//@property (nonatomic, strong) B b; //原來的代碼
@property (nonatomic, weak) B b; //將 strong 引用改成 weak 引用
@end
弱引用:weak類型的指針也可以指向?qū)ο笮托停遣⒉粫钟性搶ο蟆2⑶以趯ο蟊会尫诺臅r候全蝶,自動被設(shè)置為 nil
那么 block 的循環(huán)引用也跟這個類似闹蒜,只不過是其中一方變成一個 block:
block 循環(huán)引用什么時候會發(fā)生?舉一個簡單的栗子抑淫,有一個 BBBViewController 類型的對象 bCon绷落,通過一個 navigation controller 來加載顯示:
@interface BBBViewController : UIViewController
@property (nonatomic, strong) UILabel *label;
@property (strong, nonatomic) VoidBlock block;
@end
@implementation BBBViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.block = ^() {
self.label.text = @"circular reference";
};
}
@end
在這種情況下,當(dāng) bCon 從 navigationCon 中被 pop 掉時始苇,bCon 并不會被釋放(可通過在 dealloc 中的斷點來觀察)砌烁。簡單一點的情況編譯器還能檢查出來,然后給你一個警告:
要是引用層級深一點催式,編譯器可就不知道了:
要知道 block 為什么會引用了 self函喉,就要稍微了解一下:
Objective-C block的實現(xiàn)
關(guān)于 block 的實現(xiàn),還有對外部變量的引用方式荣月,在文末的參考文章可以找到非常詳盡的說明管呵,大概來說就是:
block 實例會保存引用自外部的變量『硗可能是變量的值撇寞,也有可能是變量的指針(比如使用了 __block 修飾的變量)
那么,對于self呢堂氯。參考文獻中的例子都沒有涉及到這一點蔑担。而我們實際上經(jīng)常使用 block 作為一些對象的回調(diào),其中往往涉及到當(dāng)前類的屬性設(shè)置咽白。下面看一個簡單的例子:
//.h
@interface CCCObject : NSObject
@property (nonatomic) int value;
@end
//.m
typedef void (^VoidBlock)();
@implementation CCCObject
- (id)init
{
if (self = [super init]) {
VoidBlock block = ^() {
int i = 1024;
};
block();
}
return self;
}
@end
在 CCCObject 的 init 函數(shù)中定義了一個 block啤握,這個 block 聲明并初始化了一個局部變量,用 clang 將它翻譯成 C++ 代碼:
clang -rewrite-objc CCCObject.m
我們可以得到一個擁有 10W+ 行代碼的文件晶框,摘取主要部分如下:
CCCObjectinit_block_impl_0
這是 init 函數(shù)中 block 的結(jié)構(gòu)定義排抬。其中 impl 是每個 block 結(jié)構(gòu)都會擁有的變量,它包含一些基本的信息:
isa 指針授段,所有對象都會擁有該指針蹲蒲,用來實現(xiàn)對象相關(guān)的功能;
flags侵贵,用來按 bit 位表示一些 block 的附加信息届搁,在 block 進行 copy 的時候會使用到;
funcPtr,函數(shù)指針卡睦,指向 block 中實際實現(xiàn)的函數(shù)調(diào)用地址宴胧,例如本例中的 __CCCObject__init_block_func_0
desc,block 的附加描述信息表锻,例如 size恕齐,還有 copy 跟 dispose 函數(shù)的指針
CCCObjectinit_block_func_0
從這個函數(shù)里面的那句代碼就可以知道,這就是 block 在實際執(zhí)行的時候調(diào)用的業(yè)務(wù)函數(shù)瞬逊。
接下來對 block 做一些修改显歧,讓它修改 屬性 value 的值:
- (id)init
{
if (self = [super init]) {
VoidBlock block = ^() {
self.value = 1024;
};
block();
}
return self;
}
接著還是 clang 一下,摘取主要代碼部分如下:
留意 block 結(jié)構(gòu)體的實現(xiàn)码耐,里面多了一個成員變量 CCCObject *self追迟,用來保存所引用的 CCCObject 對象,在實現(xiàn)函數(shù)中通過 objc_msgSend 函數(shù)來對 value 屬性進行設(shè)置骚腥。換句話說敦间,block 強引用了 self.
有興趣的朋友可以再看看 _ICCCObject_init 這個函數(shù),看起來好像一團糟的樣子束铭,因為它糅雜了 C 語言里面的兩個最強利器:強制類型轉(zhuǎn)換 以及 指針廓块,而且還用得不少,看上去很糟糕契沫。但它實際上做的主要事情是:
通過構(gòu)造函數(shù) __CCCObject__init_block_impl_0 構(gòu)造一個 block 對象带猴,構(gòu)造時候需要將實際業(yè)務(wù)函數(shù) ***_func_0 作為其中一個參數(shù)傳入;
然后通過這個對象中的函數(shù)指針懈万,來調(diào)用業(yè)務(wù)函數(shù)拴清,需要將這個 block 結(jié)構(gòu)自身作為參數(shù)傳入;
那么如果沒有修改 self 的屬性会通,而是單純的將 self 作為值傳遞到一個局部變量中呢:
- (id)init
{
if (self = [super init]) {
VoidBlock block = ^() {
CCCObject *cccObject = self;
};
block();
}
return self;
}
同樣會強引用 self口予。
解除 block 引用循環(huán)
所以,如果在 block 中使用到 self 關(guān)鍵字(有例外涕侈,下詳)沪停,而 self 通過某種方式引用了這個 block,那么引用循環(huán)就會產(chǎn)生裳涛。那么如何打破 block 的引用循環(huán)木张,經(jīng)典的做法是:
- (id)init
{
if (self = [super init]) {
__weak __typeof(&*self) weakSelf = self;
VoidBlock block = ^() {
weakSelf.value = 1024;
};
block();
}
return self;
}
而 extobjc 這個庫中的 EXTScope.h 定義了兩個更方便的宏,可以更加優(yōu)雅地解決 block 循環(huán)引用問題: weakify 和 strongify端三。最新版本的實現(xiàn)方法有點復(fù)雜舷礼,先看看傳統(tǒng)的寫法(以下內(nèi)容主要參考自這里):
#define weakify( x ) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
autoreleasepool{} __weak __typeof__(x) __weak_##x##__ = x; \
_Pragma("clang diagnostic pop")
#define strongify( x ) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
try{} @finally{} __typeof__(x) x = __weak_##x##__; \
_Pragma("clang diagnostic pop")
使用方法:
- (id)init
{
if (self = [super init]) {
@weakify(self) // 定義了一個__weak的self_weak_變量
self.block = ^() {
@strongify(self) // 局域定義了一個__strong 的同名 self 指針指向self_weak
// 至于為什么沒有報警告,看定義就知道了
self.value = 1024;
};
self.block();
}
return self;
}
原理就正如注釋所寫的郊闯,轉(zhuǎn)換成原生代碼的話:
- (id)init
{
if (self = [super init]) {
__weak typeof(self) weakSelf = self;
self.block = ^() {
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf.value = 1024;
};
self.block();
}
return self;
}
解釋一下:
在 block 外部定義一個 weakSelf且轨,供下面的 block 進行引用;
在 block 中定義一個局部同名 self 指針浮声,通過 weakSelf strong 引用自身,保證 block 執(zhí)行的過程中旋奢,同名覆蓋的變量(也就是 self)不會被釋放掉;
由于 block 對 self 是弱引用,在同名 strong 局部變量被釋放之后然痊,block 也就結(jié)束了所有對 self 的強引用至朗;