Block的引用循環(huán)問題 (ARC & non-ARC)
2014-03-25 14:56 24799人閱讀 評論(2) 收藏 舉報
分類: iPhone(315)
目錄(?)[+]
2010年WWDC發(fā)布iOS4時Apple對Objective-C進行了一次重要的升級:支持Block。說到底這東西就是閉包绊含,其他高級語音例如Java和C++已有支持宝冕,第一次使用Block感覺滿簡單好用的允瞧,但是慢慢也遇到很多坑境蜕。本文聊聊ARC和non-ARC下Block使用中的引用循環(huán)問題,最近遇到了好幾次這種問題掉房,還是深入記錄下茫陆。先來套題目熱熱身,貌似能夠全部答對的人蠻少的
Block實現(xiàn)原理
首先探究下Block的實現(xiàn)原理逃延,由于Objective-C是C語言的超集览妖,既然OC中的NSObject對象其實是由C語言的struct+isa指針實現(xiàn)的,那么Block的內(nèi)部實現(xiàn)估計也一樣揽祥,以下三篇Blog對Block的實現(xiàn)機制做了詳細研究:
A look inside blocks: Episode 1
A look inside blocks: Episode 2
A look inside blocks: Episode 3
雖然實現(xiàn)細節(jié)看著頭痛讽膏,不過發(fā)現(xiàn)Block果然是和OC中的NSObject類似,也是用struct實現(xiàn)出來的東西拄丰。這個是LLVM項目compiler-rt分析的block頭文Block_private.h頭文件中關(guān)于Block的struct聲明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
我們發(fā)現(xiàn)Block_layout中也有一個isa指針府树,像極了NSobject內(nèi)部實現(xiàn)struct中的isa指針。這里的isa可能指向三種類型之一的Block:
_NSConcreteGlobalBlock:全局類型Block料按,在編譯器就已經(jīng)確定奄侠,直接放在代碼段__TEXT上。直接在NSLog中打印的類型為__NSGlobalBlock__载矿。
_NSConcreteStackBlock:位于棧上分配的Block垄潮,即__NSStackBlock__。
_NSConcreteMallocBlock:位于堆上分配的Block闷盔,即__NSMallocBlock__魂挂。
為什么會有這么多種類呢?首先來看全局類型Block馁筐,看例子:
1
2
3
4
5
6
7
8
9
10
11
12
void addBlock(NSMutableArray *array) {
[array addObject:^{
printf("global block\n");
}];
}
void example() {
NSMutableArray *array = [NSMutableArray array];
addBlock(array);
void (^block)() = [array objectAtIndex:0];
block();
}
為什么addBlock中添加到array中的Block屬于全局Block呢涂召?因為它不需要運行時(Runtime)任何的狀態(tài)來改變行為,不需要放在堆上或者棧上敏沉,直接編譯后在代碼段中即可果正,就像個c函數(shù)一樣炎码。這種類型的Block在ARC和non-ARC情況下沒有差別。
這個Block訪問了作用域外的變量d秋泳,在實現(xiàn)上就是這個block會多一個成員變量對應(yīng)這個d潦闲,在賦值block時會將方法exmpale中的d變量值復(fù)制到成員變量中,從而實現(xiàn)訪問迫皱。
1
2
3
4
5
6
7
void example() {
int d = 5;
void (^block)() = ^() {
printf("%d\n", d);
};
block();
}
如果要修改d呢歉闰?:
1
2
3
4
5
6
7
8
9
void example() {
int d = 5;
void (^block)() = ^() {
d++;
printf("%d\n", d);
};
block();
printf("%d\n", d);
}
由于局部變量d和這個block的實現(xiàn)不在同一作用域,僅僅在調(diào)用過程中用到了值傳遞卓起,所以不能直接修改和敬,而需要加一個標(biāo)識符__block int d = 5;,那么block就可以實現(xiàn)對這個局部變量的修改了戏阅。如果是這種block標(biāo)識的變量昼弟,在Block實現(xiàn)中不再是簡單的一個成員變量,而是對應(yīng)一個新的結(jié)構(gòu)體表示這個block變量奕筐。block的本質(zhì)是引入了一個新的Block_byref{$var_name}{$index}結(jié)構(gòu)體舱痘,被block關(guān)鍵字修飾的變量就被放到這個結(jié)構(gòu)體中。另外离赫,block結(jié)構(gòu)體通過引入Block_byref{$var_name}{$index}指針類型的成員芭逝,得以間接訪問到Block的外部變量。這樣對Block外的變量訪問從值傳遞轉(zhuǎn)變?yōu)橐迷ㄐ兀瑥亩辛诵薷膬?nèi)容的能力旬盯。
正常我們使用Block是在棧上生成的,離開了棧作用域便釋放了蹬刷,如果copy一個Block,那么會將這個Block copy到堆上分配频丘,這樣就不再受棧的限制办成,可以隨意使用啦。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef void (^TestBlock)();
TestBlock getBlock() {
char e = 'E';
void (^returnedBlock)() = ^{
printf("%c\n", e);
};
return returnedBlock;
}
void example() {
TestBlock block = getBlock();
block();
}
函數(shù)getBlock中聲明并賦值的returnedBlock搂漠,一開始是在棧上分配的迂卢,屬于NSStackBlock,如果是non-ARC情況下return這個NSStackBlock桐汤,那么其實已經(jīng)被銷毀了而克,在函數(shù)中example()使用時就會crash。如果是ARC情況下怔毛,getBlock返回的block會自動copy到堆上员萍,那么block的類型就是NSMallocBlock,可以在example()中繼續(xù)使用拣度。要在Non-ARC情況下正常運行碎绎,那么就應(yīng)該修改為:
1
2
3
4
5
6
7
TestBlock getBlock() {
char e = 'E';
void (^returnedBlock)() = ^{
printf("%c\n", e);
};
return [[returnedBlock copy] autorelease];
}
Block中的循環(huán)引用問題
扯了這么多螃壤,回到Block的循環(huán)引用問題,由于我們很多行為會導(dǎo)致Block的copy筋帖,而當(dāng)Block被copy時奸晴,會對block中用到的對象產(chǎn)生強引用(ARC下)或者引用計數(shù)加一(non-ARC下)。
如果遇到這種情況:
1
2
3
4
5
6
7
8
9
@property(nonatomic, readwrite, copy) completionBlock completionBlock;
//========================================
self.completionBlock = ^ {
if (self.success) {
self.success(self.responseData);
}
}
};
對象有一個Block屬性日麸,然而這個Block屬性中又引用了對象的其他成員變量寄啼,那么就會對這個變量本身產(chǎn)生強應(yīng)用,那么變量本身和他自己的Block屬性就形成了循環(huán)引用代箭。在ARC下需要修改成這樣:
1
2
3
4
5
6
7
8
9
@property(nonatomic, readwrite, copy) completionBlock completionBlock;
//========================================
__weak typeof(self) weakSelf = self;
self.completionBlock = ^ {
if (weakSelf.success) {
weakSelf.success(weakSelf.responseData);
}
};
也就是生成一個對自身對象的弱引用墩划,如果是倒霉催的項目還需要支持iOS4.3,就用__unsafe_unretained替代__weak梢卸。如果是non-ARC環(huán)境下就將__weak替換為__block即可走诞。non-ARC情況下,__block變量的含義是在Block中引入一個新的結(jié)構(gòu)體成員變量指向這個__block變量蛤高,那么__block typeof(self) weakSelf = self;就表示Block別再對self對象retain啦蚣旱,這就打破了循環(huán)引用邪媳。