
近來(lái)把《iOS與OS X多線(xiàn)程和內(nèi)存管理》這本書(shū)又掏出來(lái)看了一遍,這本書(shū)前前后后加起來(lái)看了能有三四遍了,每次看都有新的理解?,F(xiàn)在就把個(gè)人對(duì)Block的一些理解記錄下來(lái)。
今天的內(nèi)容中你會(huì)看到:
- Block是什么
- Block的實(shí)質(zhì)
- 關(guān)于Block對(duì)外部的賦值操作
- Block類(lèi)型及其存儲(chǔ)域
- __block說(shuō)明符
- 關(guān)于Block引起的循環(huán)引用
Block是什么?
帶有自動(dòng)變量的匿名函數(shù)。
————引自《iOS與OS X多線(xiàn)程和內(nèi)存管理》
為什么這么說(shuō)呢?
我們分別從匿名函數(shù)和帶自動(dòng)變量?jī)蓚€(gè)角度來(lái)說(shuō)。
- 1.匿名函數(shù)
首先,Blocks是C語(yǔ)言的擴(kuò)充功能。
C語(yǔ)言中函數(shù)是這個(gè)樣子的:
void func() {
printf("hello world");
}
如上,是一個(gè)C語(yǔ)言函數(shù),而block是什么形式呢?
^void () {
printf("hello world");
}
這種不帶函數(shù)名的函數(shù)就是所謂的匿名函數(shù)。
- 2.帶自動(dòng)變量
還是要從C語(yǔ)言函數(shù)中說(shuō)起。
int a = 10;
void func (int b) {
printf("%d + %d = %d",a,b,a+b);
}
int main(int argc, char * argv[]) {
func(5);
return 0;
}
上述的代碼主要想說(shuō)明一件事,C語(yǔ)言函數(shù)中,函數(shù)體中使用的函數(shù)外部變量只有兩種:函數(shù)參數(shù)即全局變量。
我們?cè)倏纯碆lock是如何使用的。
int main(int argc, char * argv[]) {
int a = 10;
void(^block)(int) = ^(int b) {
printf("%d + %d = %d\n",a,b,a+b);
};
block(5);
return 0;
}
我們看到,在此例中a已經(jīng)不是全局變量了,而是一個(gè)局部變量,也就是自動(dòng)變量。然而block卻可以正常使用,為什么呢?因?yàn)閎lock內(nèi)部維護(hù)了一個(gè)變量a的值,所以執(zhí)行正確。這里你先不用糾結(jié),下面會(huì)有源碼。由上我們就知道了什么叫做帶自動(dòng)變量了。
Block的實(shí)質(zhì)
想要看Block的實(shí)質(zhì)我們還是Block的實(shí)現(xiàn)過(guò)程。我們還是要借助clang。
#include <stdio.h>
int main(int argc, char * argv[]) {
int a = 10;
void(^block)(int) = ^(int b) {
printf("%d + %d = %d\n",a,b,a+b);
};
block(5);
return 0;
}
還是這個(gè)簡(jiǎn)單的函數(shù),我們借助clang來(lái)轉(zhuǎn)換一下。這里為了稍后方便,我們盡量刪除其他無(wú)用頭文件,引入必要頭文件。
clang -rewrite-objc main.m
轉(zhuǎn)換完成后我們會(huì)發(fā)現(xiàn)main.m同文件夾下多了一個(gè)main.cpp的文件。
打開(kāi)這個(gè)文件我們就會(huì)看到轉(zhuǎn)換后的源碼,而老司機(jī)讓同學(xué)們換頭文件的原因你也應(yīng)該明白了,引入的頭文件中一些相關(guān)代碼也會(huì)在轉(zhuǎn)換的文件中。如果你跟老司機(jī)一樣只引入了stdio.h的話(huà),那么現(xiàn)在command + L跳轉(zhuǎn)到第62行,復(fù)制62行至67行,command + 下調(diào)至文件底部粘貼,再跳至510行,command + shift + 上選中上面所有代碼,delete刪除后就剩下干貨了,大概是這個(gè)樣子的:

恩,我們看到這就是block的相關(guān)實(shí)現(xiàn)。
首先我們看block結(jié)構(gòu)體中,三個(gè)成員變量,一個(gè)構(gòu)造函數(shù)。

可以看到第一個(gè)成員變量是__block_impl的結(jié)構(gòu)體,其中有指向block實(shí)現(xiàn)函數(shù)的函數(shù)指針,第二個(gè)成員變量是__main_block_desc_0,用來(lái)負(fù)責(zé)管理block的內(nèi)存管理。第三個(gè)成員變量int型變量a。
老司機(jī)在這里解釋一下,int a這個(gè)成員變量就是上面提到的帶有的自動(dòng)變量。因?yàn)?strong>block內(nèi)部引用了外部的自動(dòng)變量,所以在block結(jié)構(gòu)體中多了一個(gè)同類(lèi)型同名的成員變量。同樣,如果沒(méi)有引入外部的自動(dòng)變量的話(huà)此處block結(jié)構(gòu)體中也不會(huì)有這第三個(gè)成員變量。
現(xiàn)在我們將目光集中到main函數(shù)中??梢钥吹剑谝恍新暶髁艘粋€(gè)局域變量,第二行調(diào)用了block的構(gòu)造函數(shù),將block對(duì)應(yīng)的函數(shù)指針和Desc以及局部變量傳給了block。
然后我們看到第三行調(diào)用block結(jié)構(gòu)體中的函數(shù)指針指向的函數(shù),并把block自身及參數(shù)傳給了函數(shù)指針指向的函數(shù)。
轉(zhuǎn)過(guò)來(lái)看block指向的函數(shù),函數(shù)中首先從block自身中取出捕獲的自動(dòng)變量a復(fù)制給一個(gè)臨時(shí)變量,同時(shí)執(zhí)行原本block中的函數(shù)體。
至此就完成了一次block的調(diào)用過(guò)程。
這里我們要注意一下捕獲的自動(dòng)變量:
所謂捕獲的自動(dòng)變量我們可以從兩方面來(lái)理解。
-
1.我們看到在生成block的瞬間就將自動(dòng)變量的值賦給了block。所以此時(shí)外界計(jì)時(shí)修改局部變量的值并不影響block中的值。
int main(int argc, char * argv[]) { int a = 10; void(^block)(int) = ^(int b) { printf("%d + %d = %d\n",a,b,a+b); }; a = 5; block(5);///執(zhí)行結(jié)果為15 return 0;
}
```
執(zhí)行結(jié)果為15,上面的話(huà)正是最好的解釋。
2.block中我們是不能對(duì)捕獲的變量進(jìn)行賦值操作的,只要這么做編譯器就會(huì)警告。為什么蘋(píng)果會(huì)做出這樣的限制呢?因?yàn)樵赽lock里對(duì)捕獲的自動(dòng)變量復(fù)制其實(shí)是有歧義的。因?yàn)橥ㄟ^(guò)看
__main_block_func_0內(nèi)部的實(shí)現(xiàn)我們知道,block內(nèi)部使用的都是block捕獲到自動(dòng)變量,當(dāng)然這個(gè)自動(dòng)變量是我們轉(zhuǎn)換代碼之前完全不知道的一個(gè)概念。也就是在編碼過(guò)程中我們?cè)赽lock中使用的變量與實(shí)際代碼運(yùn)行過(guò)程中block內(nèi)部操作的變量本就是兩個(gè)變量,所以在這里修改block捕獲的自動(dòng)變量的值事實(shí)上跟開(kāi)發(fā)者預(yù)期的結(jié)果完全是兩個(gè)結(jié)果。所以蘋(píng)果干脆在此就給出個(gè)警告來(lái)避免未知的錯(cuò)誤。3.雖說(shuō)不能對(duì)捕獲的自動(dòng)變量進(jìn)行賦值操作,但這并
不影響我們使用他,否則的話(huà)這個(gè)自動(dòng)變量捕獲到也沒(méi)有什么用了。這點(diǎn)很好理解,沒(méi)什么好解釋的。
說(shuō)到這里,是時(shí)候來(lái)一個(gè)本節(jié)的扣題了,所以說(shuō)block的實(shí)質(zhì)事實(shí)上就是一個(gè)結(jié)構(gòu)體,而且是一個(gè)可以根據(jù)自身捕獲的自動(dòng)變量個(gè)數(shù)自動(dòng)添加自身成員變量的結(jié)構(gòu)體。更多情況下,其實(shí)你把它考慮成對(duì)象更好。
關(guān)于Block對(duì)外部的賦值操作
上文中老司機(jī)說(shuō)到,Block不能對(duì)其捕獲的局部(非靜態(tài))變量的值進(jìn)行賦值操作。既然有這些限制,那么一定有可以Block中可以做賦值操作的變量,他們都有誰(shuí)呢?
- 靜態(tài)變量
- 全局變量
- __block說(shuō)明符修飾的變量
還是針對(duì)帶有自動(dòng)變量的匿名函數(shù)這句話(huà)來(lái)講。這一節(jié)我們來(lái)探討一下Block是如何使用外部變量的。我們知道Block截獲變量的意義在于想要使用Block作用于內(nèi)無(wú)法使用的變量,所以他要截獲變量。接下來(lái)老司機(jī)圍繞著這句話(huà)從各種變量類(lèi)型做深入的展開(kāi)。
- 1.僅使用參數(shù)的Block
int main(int argc, char * argv[]) {
void(^block)(int) = ^(int a) {
a = 10;
printf("block : a = %d\n",a);
};
int a = 5;
block(a);
printf("a = %d",a);
return 0;
}
///clang轉(zhuǎn)換后的形式
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a) {
a = 10;
printf("block : a = %d\n",a);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
void(*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
int a = 5;
((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, a);
printf("a = %d",a);
return 0;
}
由于使用的是函數(shù)的參數(shù),是在Block作用域內(nèi)可以使用的,所以Block沒(méi)有對(duì)變量進(jìn)行截獲。這個(gè)Block基本就是最簡(jiǎn)單的函數(shù)。
- 2.使用局部變量(非靜態(tài))
int main(int argc, char * argv[]) {
int a = 10;
void(^block)() = ^() {
printf("n = %d",a);
};
block();
return 0;
}
///clang轉(zhuǎn)換后
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("n = %d",a);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
int a = 10;
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
我們看到,這里截獲了這個(gè)局部變量,具體原因在上述內(nèi)容中有講到過(guò),此處不再贅述。
- 3.局部靜態(tài)變量
我們知道,靜態(tài)變量存儲(chǔ)在靜態(tài)區(qū),只創(chuàng)建一次,隨后使用的同名變量均應(yīng)指向同一地址。由靜態(tài)變量的特性我們應(yīng)該知道,如果Block截獲了一個(gè)靜態(tài)局域變量,并在Block中對(duì)其值進(jìn)行了更改,這個(gè)操作應(yīng)該是有效的,他應(yīng)該改變?cè)撟兞康闹?。我們看下他是如何?shí)現(xiàn)的?
int main(int argc, char * argv[]) {
static int a = 10;
void(^block)() = ^() {
a = 20;
};
block();
printf("a = %d",a);
return 0;
}
///clang轉(zhuǎn)換后
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *a = __cself->a; // bound by copy
(*a) = 20;
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
static int a = 10;
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
printf("a = %d",a);
return 0;
}
我們看到了,Block截獲的是局部靜態(tài)變量的指針。這個(gè)思路跟C語(yǔ)言中函數(shù)一樣。C語(yǔ)言中我們想更改實(shí)參的值時(shí)也是通過(guò)傳址的方式實(shí)現(xiàn)的。形如:
void mySwap(int * a,int * b);
int main(int argc, char * argv[]) {
int a = 1;
int b = 2;
mySwap(&a, &b);
printf("a = %d",a);
return 0;
}
void mySwap(int * a,int * b) {
int temp = *a;
*a = *b;
*b = temp;
}
4.全局變量(靜態(tài)與非靜態(tài))
老司機(jī)上面說(shuō)過(guò),Block捕獲變量是為了在Block中使用其作用域外的變量,那么全局變量本身作用在區(qū)域,Block可以使用,故不需要對(duì)全局變量進(jìn)行捕獲。以下以全局靜態(tài)變量為例。
static int a = 10;
int main(int argc, char * argv[]) {
void(^block)() = ^{
a = 20;
};
block();
printf("a = %d",a);
return 0;
}
///clang 轉(zhuǎn)換后
static int a = 10;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
a = 20;
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
printf("a = %d",a);
return 0;
}
- 5.__block修飾的變量
我們知道,被__block修飾的局部變量,在Block內(nèi)部對(duì)其進(jìn)行賦值操作是可以的,那么他是如何實(shí)現(xiàn)的呢?
int main(int argc, char * argv[]) {
__block int a = 10;
void(^block)() = ^{
a = 20;
};
block();
printf("%d",a);
return 0;
}
///clang 轉(zhuǎn)換后
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 20;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
printf("%d",(a.__forwarding->a));
return 0;
}
我們看到__block修飾的變量會(huì)自動(dòng)為其生成一個(gè)結(jié)構(gòu)體,并在之后對(duì)變量的操作使用的都是結(jié)構(gòu)體中持有的變量a。而后Block捕獲了結(jié)構(gòu)體,Block中對(duì)變量的復(fù)制也映射成了對(duì)結(jié)構(gòu)體內(nèi)部變量的賦值。我們可以發(fā)現(xiàn),在上面的例子中,不僅生成了一個(gè)__block變量的結(jié)構(gòu)體,還多了__main_block_copy_0和__main_block_dispose_0兩個(gè)函數(shù),的具體作用我們稍后再表。
Block類(lèi)型及其存儲(chǔ)域
首先應(yīng)該了解一下Block的三種類(lèi)型:
- _NSConcreteStackBlock///棧區(qū)Block
- _NSConcreteMallocBlock///堆區(qū)Block
- _NSConcreteGlobalBlock///全局Block
我們?cè)O(shè)想這樣一種情況,上述的例子中,我們看到我們都是在main函數(shù)中聲明的Block,也就是說(shuō)他其實(shí)block對(duì)象其實(shí)是一個(gè)局域變量,那么他一定會(huì)被存儲(chǔ)在棧上。也就是說(shuō)當(dāng)出了變量的作用于,也就是main函數(shù)結(jié)束,block對(duì)象就會(huì)被銷(xiāo)毀。這時(shí)我們的Block即為_NSConcreteStackBlock。我們可以從__block_impl結(jié)構(gòu)體中的isa指針看到上述例子中的Block均為_NSConcreteStackBlock類(lèi)型。
但是平時(shí)我們使用block的時(shí)候還有這么一種情況,即并不是在聲明的地方立即使用,而是在等待某個(gè)時(shí)機(jī)從而進(jìn)行回調(diào)。而此時(shí)一般是已經(jīng)出了block對(duì)象的作用域,如果跟之前一樣是棧區(qū)block的話(huà)顯然block已經(jīng)被銷(xiāo)毀,此時(shí)進(jìn)行回調(diào)只會(huì)引起crash。這時(shí)我們就需要_NSConcreteMallocBlock區(qū)的block了,即堆區(qū)Block。回想我們持有block的時(shí)候使用什么修飾符呢?copy對(duì)吧,而block對(duì)象執(zhí)行copy操作就是將其按需復(fù)制到堆區(qū)。
我們看下下面的例子:
#import <Foundation/Foundation.h>
int globalVar = 100;
void(^globalBlock)() = ^{
NSLog(@"global block beyond main");
};
int main(int argc, char * argv[]) {
int a = 10;
NSLog(@"stack block: %@",^{NSLog(@"here a = %d",a);});
NSLog(@"malloc block: %@",[^{NSLog(@"here a = %d",a);} copy]);
NSLog(@"global block: %@",^{NSLog(@"I use nothing");});
NSLog(@"global block beyong main: %@",globalBlock);
NSLog(@"global block use globalVar: %@",^{NSLog(@"%d",globalVar);});
return 0;
}
///輸出:
stack block: <__NSStackBlock__: 0x7fff5d7cd578>
malloc block: <__NSMallocBlock__: 0x60000004ef70>
global block: <__NSGlobalBlock__: 0x1024320d0>
global block beyong main: <__NSGlobalBlock__: 0x102432050>
global block use globalVar: <__NSGlobalBlock__: 0x102432110>
此處我們可以看到三種block類(lèi)型。從源碼我們可以知道默認(rèn)生成的block均為_NSConcreteStackBlock類(lèi)型,而后執(zhí)行了copy操作的block為_NSConcreteMallocBlock類(lèi)型,后面三個(gè)均為_NSConcreteGlobalBlock類(lèi)型。
這里我們先說(shuō)_NSConcreteGlobalBlock類(lèi)型的Block。在全局范圍內(nèi)聲明的Block即為全局Block,并且沒(méi)有引入自動(dòng)變量的也為全局Block。
現(xiàn)在我們知道了,調(diào)用過(guò)copy方法的block會(huì)被復(fù)制到堆區(qū),堆區(qū)的Block均為_NSConcreteMallocBlock類(lèi)型。那么什么情況下block會(huì)執(zhí)行copy方法呢?
其實(shí)我們可以從上述的分析中猜到,當(dāng)block需要在其作用域外使用的我們應(yīng)該將其復(fù)制到堆區(qū)。例如block作為函數(shù)返回值的時(shí)候,這時(shí)候編譯器會(huì)按需調(diào)用copy方法:
typedef void(^voidBlock)();
voidBlock func();
int main(int argc, char * argv[]) {
NSLog(@"the return value of func:%@",func());
return 0;
}
voidBlock func() {
int a = 10;
NSLog(@"the block in func:%@",^{NSLog(@"block in func : a = %d",a);});
return ^{
NSLog(@"block in func : a = %d",a);
};
}
///輸出:
the block in func:<__NSStackBlock__: 0x7fff56877558>
the return value of func:<__NSMallocBlock__: 0x6080000486a0>
此例中我們看到函數(shù)體中,輸出了一個(gè)Block其為棧區(qū)Block,但是當(dāng)將同樣的Block作為返回值返回到main函數(shù)中的時(shí)候,他變成了堆區(qū)Block。
同學(xué)們不要說(shuō)我這不是一個(gè)Block,我應(yīng)該生成一個(gè)Block將其賦值,Log一下,在返回出去。這個(gè)真不是我不賦值,我不能啊,因?yàn)樵?strong>ARC中賦值的時(shí)候如果不附加修飾符的話(huà)默認(rèn)認(rèn)為生成的變量是以__strong修飾符修飾的,而編譯器遇到__strong修飾符會(huì)自動(dòng)copy。。。我怎么給你做例子啊。。。反正老司機(jī)這么寫(xiě)雖然不是同一個(gè)block,但是應(yīng)該是同一類(lèi)型block,足以說(shuō)明問(wèn)題。另外老司機(jī)說(shuō)過(guò),編譯器會(huì)按需調(diào)用copy方法。也就是說(shuō)棧區(qū)block會(huì)出作用域銷(xiāo)毀,全局block并不會(huì),所以如果返回值是一個(gè)全局block的話(huà),則不會(huì)調(diào)用copy方法。
此外以下兩種情況也會(huì)由系統(tǒng)為我們調(diào)用copy方法:
- Cocoa框架的方法且方法名中含有usingBlock等時(shí)
- GCD的API
還有就是顯示調(diào)用copy方法的時(shí)候,另外如果將其賦值給有copy修飾符修飾的屬性的話(huà)也會(huì)調(diào)用copy方法。
然而什么時(shí)候應(yīng)該調(diào)用copy方法呢?我們先來(lái)看下不同類(lèi)型block調(diào)用copy方法會(huì)有什么行為。
| Block類(lèi)型 | 副本源的配置存儲(chǔ)域 | 復(fù)制效果 |
|---|---|---|
| _NSConcreteStackBlock | 棧 | 從棧復(fù)制到堆 |
| _NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)域 | 什么也不做 |
| _NSConcreteMallocBlock | 堆 | 引用計(jì)數(shù)增加 |
不管Block配置在何處,用copy方法復(fù)制都不會(huì)引起任何問(wèn)題。在不確定時(shí)調(diào)用copy方法即可。
————引自《iOS與OS X多線(xiàn)程和內(nèi)存管理》
但是在我們確定的時(shí)候,還是要根據(jù)需要調(diào)用copy方法,不要盲目調(diào)用copy方法,畢竟這個(gè)方法是十分占用CPU資源的。
__block說(shuō)明符
上文中,老司機(jī)已經(jīng)講述了block對(duì)象在調(diào)用copy方法時(shí)候的行為。然而__block說(shuō)明符修飾的變量與block對(duì)象基本一致。
| __block變量的配置存儲(chǔ)域 | Block從棧復(fù)制到堆時(shí)的影響 |
|---|---|
| 棧 | 從棧復(fù)制到堆并被Block持有 |
| 堆 | 被Block持有 |
正如老司機(jī)在上文中提到的,被__block說(shuō)明符的變量會(huì)自動(dòng)生成一個(gè)結(jié)構(gòu)體。
值得一提的是三個(gè)地方:

老司機(jī)之前說(shuō)過(guò),只有被__block說(shuō)明符修飾的變量,今后使用的均為其結(jié)構(gòu)體中維護(hù)的同名成員變量,不過(guò)從源碼中我們看到,并不是簡(jiǎn)單地使用了成員變量,而是a.__forwarding->a這樣一個(gè)引用方式,這是因?yàn)槭裁茨兀?/p>
首先從__Block_byref_a_0中我們可以看到__forwarding是一個(gè)__Block_byref_a_0類(lèi)型的結(jié)構(gòu)體指針。
從main函數(shù)中第一行__block變量生成的代碼我們看出,在本例中生成__block變量a的同時(shí)將a的__forwarding指向了a自身。這樣a.__forwarding->a最終還是指向了__block變量a結(jié)構(gòu)體中的成員變量a。
既然這樣,就一定存在__forwarding并不指向block變量自身的情況,故此才需要__forwarding存在來(lái)保證時(shí)刻能取到一個(gè)正確的值。而上文中提到的調(diào)用copy方法的時(shí)候,就會(huì)對(duì)__forwarding指針進(jìn)行操作。

由上圖我們可以看到,當(dāng)調(diào)用copy方法后,__forwarding指針指向堆中的__block變量。而堆中的__block變量的__forwarding指針則指向自身。
同時(shí)我們知道,block其實(shí)是對(duì)c語(yǔ)言的擴(kuò)充,然而OC中我們使用的是引用計(jì)數(shù)來(lái)管理對(duì)象生命周期,而不是GC。所以事實(shí)上Block需要自行管理內(nèi)存。那么當(dāng)我們的Block捕獲了一個(gè)對(duì)象時(shí),他又是如何管理其引用計(jì)數(shù)的呢?
上文中老司機(jī)有提到過(guò)__main_block_copy和__main_block_dispose兩個(gè)函數(shù)。當(dāng)Block結(jié)構(gòu)體中捕獲到的對(duì)象需要retain的時(shí)候則調(diào)用__main_block_copy方法增加引用計(jì)數(shù),當(dāng)其需要釋放的時(shí)候則調(diào)用__main_block_dispose釋放對(duì)象。所以當(dāng)block從棧上復(fù)制到堆的時(shí)候會(huì)調(diào)用copy函數(shù),而對(duì)上的block被釋放時(shí)調(diào)用dispose函數(shù)。
關(guān)于Block引起的循環(huán)引用
一直以來(lái),Block引起的循環(huán)引用都讓不少初級(jí)工程師,甚至包括一些中級(jí)工程師(索性就叫他中級(jí)吧。。。)談虎色變。他們不知道Block是如何引起循環(huán)引用的,只知道__weak可以避免循環(huán)引用。知其然不知其所以然,鬧出一些笑話(huà)也是讓人無(wú)語(yǔ)。
首先說(shuō)一下什么是循環(huán)引用?
引用計(jì)數(shù)機(jī)制不做展開(kāi),我們只需要知道,在OC中對(duì)象是在引用計(jì)數(shù)為0的時(shí)候進(jìn)行銷(xiāo)毀的。一個(gè)對(duì)對(duì)象的強(qiáng)引用會(huì)造成一次引用計(jì)數(shù)的加一。釋放一個(gè)強(qiáng)引用會(huì)造成引用計(jì)數(shù)的減一。

上圖我們知道,對(duì)象A對(duì)對(duì)象B有一個(gè)強(qiáng)引用。當(dāng)對(duì)象A銷(xiāo)毀的時(shí)候,會(huì)釋放對(duì)對(duì)象B的強(qiáng)引用。如果此時(shí)對(duì)象B的引用計(jì)數(shù)為0則對(duì)象B被銷(xiāo)毀。

但當(dāng)出現(xiàn)上圖的情況,強(qiáng)引用出現(xiàn)了一個(gè)閉環(huán)的時(shí)候,就會(huì)造成逐個(gè)等待上一個(gè)強(qiáng)引用釋放信號(hào),然而閉環(huán)導(dǎo)致任意一個(gè)對(duì)象都不會(huì)釋放對(duì)下一個(gè)對(duì)象的強(qiáng)引用,這就是循環(huán)引用。
明確一點(diǎn),造成循環(huán)引用的必要條件的閉環(huán),所以循環(huán)引用不僅可以發(fā)生在兩個(gè)對(duì)象之間,可以是多個(gè)對(duì)象,甚至可以是一個(gè)對(duì)象。

由此看來(lái)Block引起循環(huán)引用的原因就很明白了,Block對(duì)內(nèi)部使用的自動(dòng)變量造成一個(gè)強(qiáng)引用,而如果這個(gè)自動(dòng)變量恰好對(duì)Block也有強(qiáng)引用的話(huà)就會(huì)造成循環(huán)引用。
既然知道了循環(huán)引用的起因,那么我們只要打破引用的閉環(huán)就可以輕松解決。兩個(gè)思路,一個(gè)是從最開(kāi)始就不讓強(qiáng)引用成為閉環(huán),使用弱引用。另一個(gè)思路是找到一個(gè)合適的時(shí)機(jī)主動(dòng)釋放一個(gè)強(qiáng)引用,打破閉環(huán)。

- 弱引用
__weak typeof(self)weakSelf = self;
self.block = ^{
NSLog(@"%@",weakSelf);
};
self.block();
上述代碼中,使用__weak生成一個(gè)弱引用變量weakSelf,保持對(duì)self的弱引用。然后Block捕獲到weakSelf,對(duì)weakSelf也是弱引用,然而卻沒(méi)有造成閉環(huán)。故避免了循環(huán)引用。

- 主動(dòng)釋放
__block id blockSelf = self;
self.block = ^{
NSLog(@"%@",blockSelf);
blockSelf = nil;
};
NSLog(@"%@",self.block);
self.block();
上述代碼中,使用__block生成一個(gè)block對(duì)象blockSelf,保持對(duì)self的強(qiáng)引用。然后Block捕獲到blockSelf,強(qiáng)引用blockSelf,由于self對(duì)block還有一個(gè)強(qiáng)引用,此時(shí)形成了一個(gè)閉環(huán)。但當(dāng)block調(diào)用的時(shí)候,內(nèi)部最后將blockSelf對(duì)象置為nil。由于blockSelf置為nil,__block對(duì)象失去強(qiáng)引用被銷(xiāo)毀,同時(shí)釋放對(duì)self的強(qiáng)引用,從而打破閉環(huán)。

不過(guò)兩種避免循環(huán)引用的方式都有各自的缺點(diǎn)。
__weak 的弱引用形式的缺點(diǎn)在于,當(dāng)block執(zhí)行的時(shí)候,由于對(duì)self是弱引用,不能保證self對(duì)象是否已經(jīng)被銷(xiāo)毀。事實(shí)上block執(zhí)行前self被銷(xiāo)毀還好,頂多是不執(zhí)行。但是如果在block執(zhí)行過(guò)程中,self被銷(xiāo)毀就會(huì)造成不可預(yù)估的后果。所以當(dāng)使用__weak的時(shí)候我們通常會(huì)看到如下結(jié)構(gòu):
__weak typeof(self)weakSelf = self;
self.block = ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
NSLog(@"%@",strongSelf);
};
self.block();
這樣的結(jié)構(gòu)可以保證在block執(zhí)行過(guò)程中,不會(huì)因?yàn)閟elf釋放引起問(wèn)題,然而如果block執(zhí)行前self被釋放后block也就沒(méi)有機(jī)會(huì)執(zhí)行了,也算是對(duì)代碼的保護(hù)。更多的關(guān)于Weak-Strong-Dance的討論可以看下這篇文章:
__weak有這樣的缺點(diǎn),為什么不適用__block等方式呢?
事實(shí)上__block同樣有著自己的煩惱,就是一定要在block體中對(duì)__block對(duì)象置為nil,且block一定要執(zhí)行才可以解決循環(huán)引用。所以開(kāi)發(fā)者要根據(jù)具體情況合理的選擇解決循環(huán)引用的方式。
至此,老司機(jī)今天的內(nèi)容也就算結(jié)束了。
參考資料:
- 《iOS與OS X多線(xiàn)程和內(nèi)存管理》
- Weak-Strong-Dance真的安全嗎?