1. 關于 Block 的幾道題
1. void exampleA() {
char a = 'A';
^{
printf("%c\n", a);
}()
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
2. void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%c\n", b);
}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block()
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
3. void exampleC_addBlockToArray(NSMutableArray *array) {
[array addObject:^{
printf("C\n");
}];
}
void exampleC() {
NSMutableArray *array = [NSMutableArray array];
exampleC_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
4. typedef void (^dBlock)();
dBlock exampleD_getBlock() {
char d = 'D';
return ^{
printf("%c\n", d);
};
}
void exampleD() {
exampleD_getBlock()();
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
5. typedef void (^eBlock)();
eBlock exampleE_getBlock() {
char e = 'E';
void (^block)() = ^{
printf("%c\n", e);
};
return block;
}
void exampleE() {
eBlock block = exampleE_getBlock();
block;
}
The example ()
A. always works.
B. only works with ARC.
C. only works without ARC.
D. never works.
結果分別為:A、B、A、B、B
解釋:
- 第一題中,Block 訪問外部變量 a,之后在棧上生成了一個同名的只讀變量 a。需要注意的是,兩個 a 的生命周期是相同的,同樣都存在于棧上。由于調(diào)用函數(shù) exampleA() 中調(diào)用了 Block,故 ARC 與非 ARC 都是可以運行的。
- 第二題中,如果不是在 ARC 模式下調(diào)用,則 Block 為
__NSStackBlock
類型 ,當在 exampleB() 中調(diào)用 Block 的時候,Block
是不可用的。在 ARC 模式下,Block 會自動 copy 到堆上面(也是自動釋放的),是__NSMallocBlock
類型的。 - 第三題中,Block 沒有訪問任何的外部變量,則 Block 為
__NSGlobalBlock
類型的。它既不在堆上也不在棧上,而是在全局區(qū)中。 - 第四題中,與第二題類似,Block 訪問了棧上的變量 d,但是在 exampleD() 函數(shù)中調(diào)用了 Block。在非 ARC 模式下,會報錯:
error: returning block that lives on the local stack
。在 ARC 模式下,Block 會自動 copy 到堆上并且自動
release,其類型為__NSMallocBlock
。 - 第五題中,與第四題其實是一樣的,只不過將 Block 返回給了一個局部變量,之后將這個局部變量返回。這樣在非 ARC 模式下編譯器不會再報錯,但是還是會出現(xiàn)問題。ARC 模式下,Block 會自動 copy 到堆上,并且自動 release,類型為
__NSMallocBlock
。
2. Block 的分類
-
__NSGlobalBlock 類型(全局 Block)
__NSGlobalBlock 類型
總結:對于沒有引用外部變量的 Block,無論是在 ARC 還是在非 ARC 下,類型都是__NSGlobalBlock
。這種類型的 Block 可以理解為一種全局的 Block,不需要考慮作用域的問題。同時,對它進行 copy 或者 retain 操作也都是無效的。 -
__NSStackBlock 類型(棧 Block)
MRC 下 __NSStackBlock 類型
ARC下 __NSStackBlock 類型
總結:對于引用了外部局部變量(注意不是局部靜態(tài)變量)的 Block,在 MRC 下如果沒有對它進行 copy 操作,它的作用域只會在定義它的函數(shù)棧內(nèi)(類型為__NSStackBlock
)。在 ARC 下,由于對象指針默認為__strong
修飾,故直接賦值的話 Block 會被 copy 到堆上。如果對象指針用__weak
修飾,則 Block 不會被拷貝到堆上面。
-
__NSMallocBlock 類型(堆 Block)
MRC 下 __NSMallocBlock 類型
ARC下 __NSMallocBlock 類型
總結:在 MRC 下,對 Block 進行 copy 操作后,Block 會被拷貝到堆上。在 ARC
下,由于對象指針默認由__strong
修飾,則程序默認會將 Block 拷貝到堆上(如何使用__weak
則程序不會拷貝 Block 到堆上)。
3. __block 關鍵字
-
默認情況
沒有使用 __block 關鍵字的情況
總結:由圖中可以得知,Block 中的 a 與外部變量 a 它們的地址并不相同,但是值是相同的。實際上 Block 中的 a 是外部變量 a 的一個拷貝,且為常量。
-
使用 __block 關鍵字
使用 __block 關鍵字
總結:由圖中可知,在使用了__block
關鍵字后,Block 中的 a 與外部變量 a 的地址相同。這種情況下 Block 中使用的就是貨真價實的外部變量 a,而且可以對
a 進行賦值操作。
-
copy 的情況
對 __block 修飾的變量的 Block 進行 copy 操作
總結:由圖可知,局部變量 a 在 Block 進行 copy 前和 copy 后的地址不同了。實際上對 Block 對象進行 copy 后,Block 中引用的變量都會被復制到堆上。而被標記為__block
的變量,實際被移動到了堆上。為什么要將 a 移動到堆上面呢?歸根結底是因為 a 被聲明了__block
。copy 之后,Block 中的 a 將被放置到堆上面,但是程序需要保持 Block 中的 a 與外部的 a 的一致性 (生命周期與作用域),故外部的 a 也就移動到了堆上面。
ps: 哪里有 copy 呢?
4. Block 的循環(huán)引用
Block 對外部引用的對象都會進行持有,直到 Block 執(zhí)行完。如果此時 Block 持有的對象正好持有 Block,則會發(fā)生內(nèi)存泄漏。
-
常見的 Block 循環(huán)引用
定義學生類
循環(huán)引用的產(chǎn)生
解釋:學生類中定義了一個 Block 屬性 work,此時學生類對象 s 持有 work 屬性。在 work 的 Block 中,又訪問了外部對象 s,則 work 持有對象 s,這就形成了一個環(huán)。
-
觀察者模式引起的循環(huán)引用
觀察者模式引起的循環(huán)引用
解釋:在通知中心的這個方法中,Block 中引用了 self 或者 self 的成員變量(無論是通過點方法還是直接訪問實例變量),Block 都會持有當前對象。如果在對象的 dealloc 方法中將通知移除,則會形成循環(huán)引用。通知中心會一直持有該對象,直到解除 Observer 的注冊。 -
NSTimer 引起的循環(huán)引用
NSTimer 循環(huán)引用
解釋:由圖中可知,self 對象對 NSTimer 對象有強引用,而且又作為 NSTimer 對象的 target。而 NSTimer 對象會一直持有 target 對象,直到 NSTimer 對象不再有效 (invalidate
方法) ,這樣就形成了一個循環(huán)引用。代碼又在 self 對象的 dealloc 方法中對 NSTimer 進行銷毀,而 self 對象的 dealloc 方法是在 self 對象將要釋放的時候調(diào)用的,所以 self 對象和 NSTimer 對象永遠都無法釋放。
-
NSURLSession 對象引起的循環(huán)引用
NSURLSession 循環(huán)引用
解釋:NSURLSession 對象會對 delegate 保持強引用,直到程序釋放或者調(diào)用了NSURLSession 對象的finishTasksAndInvalidate
方法或者invalidateAndCancel
方法。與上面的例子相似,在 dealloc 中調(diào)用 invalidate 相關方法,是無法解決循環(huán)引用的情況的,在使用的時候要多加注意。
5. Block 的底層結構
Block 的底層結構大體是由結構體以及額外的函數(shù)來構成,具體可以使用 clang 的命令編譯 .m 文件來查看:clang -rewirte-objc xxx.m
。網(wǎng)上有很多分析的文章,這里就不再一一的分析了。
6. 解決循環(huán)引用
-
通過將Block中訪問的對象設置為weak。
weak 方式
或者更加安全的方式:
更加安全的方式 -
使用完 Block 的時候及時將 Block 置為空
將 Block 置空
解釋:循環(huán)引用的發(fā)生一般都是由于 Block 持有了對象,而對象又持有了 Block 。這樣我們可以在使用完 Block 之后,立刻釋放對象對 Block 的持有,將 Block 置空就可以打破引用環(huán)。
-
根據(jù)情況將環(huán)的任一強引用置空即可
在實際的開發(fā)中,有可能存在多個節(jié)點的復雜的環(huán),除了可以將 Block 置空外,還可以打破環(huán)中的任一強引用,就可以解決掉循環(huán)引用的問題。
6. 練習
下面這四種情況中,哪個會存在內(nèi)存泄漏的情況?
- (void)test1
{
self.student = [Student new];
self.student.work = ^{
self.name = @"Hello";
};
self.student.work = nil;
}
- (void)test2
{
self.student = [Student new];
self.student.work = ^{
self.name = @"Hello";
};
self.student = nil;
}
- (void)test3
{
Student *student = [Student new];
student.work = ^{
student.name = @"小明";
};
student.work = nil;
}
- (void)test4
{
Student *student = [Student new];
student.work = ^{
student.name = @"小明";
};
student = nil;
}
7. 總結
- ARC 下 Block 會從棧上自動拷貝到堆上,原因是由于
__strong
的原因。 - Block 由于會持有外部引用的變量,容易引發(fā)循環(huán)引用的問題。解決的辦法是把環(huán)打破,或者根本不讓環(huán)生成。
- iOS 開發(fā)中使用
NSTimer
、NSURLSession
、NSNotificationCenter
的時候一定要注意。 - 需要注意,非 ARC下
__block
可以解決循環(huán)引用的問題。在 ARC 下不可以,需要使用__weak
來解決。