看了很多別人寫的Block的相關(guān)文章虐急,但是別人寫的終究是別人的箱残,看過之后沒多久也就忘了。真正的動手敲敲,仔細考究一下被辑,然后用自己的思路總結(jié)出來燎悍,才會轉(zhuǎn)化為自己的東西,才會牢記于心盼理,這也是我寫這篇文章的原因谈山。
1. Block是什么
答:是匿名函數(shù),是閉包宏怔,是對象奏路。
但是怎么理解呢?
匿名函數(shù)
:是說在定義Block時臊诊,其名稱可以省略不寫鸽粉;
閉包
:封閉的包,呵呵抓艳,看Block寫法就知道了触机。
還有人理解為閉包就是能夠讀取其它函數(shù)內(nèi)部變量的函數(shù)
這中說法怎么理解呢?
就是閉包允許一個函數(shù)訪問聲明該函數(shù)運行上下文中的變量玷或,甚至可以訪問不同運行上文中的變量儡首。
**用腳本語言來解釋:**
function funA(callback){
alert(callback());
}
function funB(){
var str = "Hello World"; // 函數(shù)funB的局部變量,函數(shù)funA的非局部變量
funA(
function(){
return str;
}
)庐椒;
}
通過上面的代碼我們可以看出椒舵,按常規(guī)思維來說,變量str是函數(shù)funB的局部變量约谈,作用域只在函數(shù)funB中笔宿,函數(shù)funA是無法訪問到str的。但是上述代碼示例中函數(shù)funA中的callback可以訪問到str棱诱,這是為什么呢泼橘,因為閉包性。
來自:http://www.cocoachina.com/ios/20150109/10891.html
**用OC語言來解釋:**
#import <Cocoa/Cocoa.h>
void logBlock( int ( ^ theBlock )( void ) )
{
NSLog( @"Closure var X: %i", theBlock() );
}
int main( void )
{
NSAutoreleasePool * pool;
int ( ^ myBlock )( void );
int x;
pool = [ [ NSAutoreleasePool alloc ] init ];
x = 42;
myBlock = ^( void )
{
return x;
};
logBlock( myBlock );
[ pool release ];
return EXIT_SUCCESS;
}
上面的代碼在main函數(shù)中聲明了一個整型迈勋,并賦值42炬灭,另外還聲明了一個block,該block會將42返回靡菇。
然后將block傳遞給logBlock函數(shù)重归,該函數(shù)會顯示出返回的值42。
即使是在函數(shù)logBlock中執(zhí)行block厦凤,而block又聲明在main函數(shù)中鼻吮,但是block仍然可以訪問到x變量,并將這個值返回较鼓。
個人覺得這個例子很low椎木,感覺只是個值傳遞的過程违柏,并不能很好的解釋閉包。
來自:http://www.cocoachina.com/ios/20130715/6599.html
對象:
:我們都知道香椎,Objective-C中的對象漱竖,其實是一個struct(結(jié)構(gòu)體),所有的對象的數(shù)據(jù)結(jié)構(gòu)里面都有一個isa指針畜伐,
那我們可以使用clang工具來看看Block的數(shù)據(jù)結(jié)構(gòu)是怎樣的:
新建一個block.m文件馍惹,里面的代碼:
int main(int argc, char * argv[]) {
void (^block)(void) = ^{
// NSLog(@"hello");
};
}
在終端使用命令clang -rewrite-objc block.m
之后,會產(chǎn)生一個block.cpp文件烤礁,其中__main_block_impl_0:
就是block的實現(xiàn)源碼讼积;
可以看到Block其實也是一個struct,內(nèi)部也有一個isa指針脚仔,指向這個Block的類型:NSConcreteStackBlock。
所以block也是對象
2. Block的寫法
- 聲明一個block:
返回值(^ 名稱)(參數(shù)列表)= ^(參數(shù)列表){
//balabala
}舆绎;
- block作為函數(shù)的參數(shù):
-(void)testAction:(返回值(^)(參數(shù)列表))名稱
- 使用
typedef
來聲明一個全局的block變量
type 返回值 (^ 名稱)(參數(shù)列表)
3. Block的類型
我們都知道block有三種類型
__NSConcreteGlobalBlock__;
__NSConcreteStackBlock__;
__NSConcreteMallocBlock__;
怎么理解這三種類型呢鲤脏?
顧名思義:
__NSConcreteGlobalBlock__://存儲在全局數(shù)據(jù)區(qū)域的block,不會訪問任何外部變量
__NSConcreteStackBlock__; //存儲在棧上的block吕朵,出棧時會被銷毀
__NSConcreteMallocBlock__;//存儲在堆上的block猎醇,當引用計數(shù)為0時會被銷毀
站在代碼表現(xiàn)形式的角度來說:
- 沒有引用外部變量的block就是
__NSConcreteGlobalBlock__
; - 引用了外部變量的block就是
__NSConcreteStackBlock__
努溃; - 對
__NSConcreteStackBlock__
進行copy
或Block_copy()
操作后硫嘶,就會變?yōu)?code>__NSConcreteMallocBlock__。
具體看代碼:
NSString * (^block1)() = ^
{
NSString *str = @"block1";
return str;
};
NSString *str = @"block1";
NSString * (^block2)() = ^
{
return str;
};
NSString * (^block3)() = Block_copy(block2);
NSString * (^block4)() = [block2 copy];
NSLog(@"block1-> %@", block1);
NSLog(@"block2-> %@", block2);
NSLog(@"block3-> %@", block3);
NSLog(@"block4-> %@", block4);
輸出日志:
**2016-06-04 10:39:14.910 BlockTest[1113:33102] block1-> <__NSGlobalBlock__: 0x5b090>**
**2016-06-04 10:39:14.910 BlockTest[1113:33102] block2-> <__NSStackBlock__: 0xbffab070>**
**2016-06-04 10:39:14.911 BlockTest[1113:33102] block3-> <__NSMallocBlock__: 0x7885c6f0>**
**2016-06-04 10:39:14.911 BlockTest[1113:33102] block4-> <__NSMallocBlock__: 0x7885c680>**
當然梧税,也可以用命令clang -rewrite-objc block.m
生成中間層代碼沦疾,看每一個block中的isa的值也可以得到block的類型。
注:在ARC環(huán)境下第队,是不存在__NSConcreteStackBlock__
的哮塞,因為ARC下,編譯器會隱式的對一些變量進行copy操作凳谦,那原先的__NSConcreteStackBlock__
就會變?yōu)?code>__NSConcreteMallocBlock__忆畅。
所以ARC環(huán)境下只會存在兩種block:
__NSConcreteGlobalBlock__
__NSConcreteMallocBlock__
4. Block引用/修改外部變量
-
引用外部變量
對于 block 外的變量引用,block 默認是將其復(fù)制到其數(shù)據(jù)結(jié)構(gòu)中來實現(xiàn)訪問的尸执。
怎么來理解這句話呢家凯?
還是用代碼來說話:
int a = 1;
int (^block1)() = ^()
{
int c = a;
return c;
};
a = 2;
NSLog(@"%d",block1());
輸出:
2016-06-17 14:19:45.550 Test[33356:4066610] 1
說明:
由于block1在聲明的時候,變量a的值已經(jīng)被復(fù)制到block1中使用了如失,所以下一行雖然變量a的值改變了绊诲,但是block1中使用的那個值是不會受到影響的。
那么引用過程到底發(fā)生了什么呢岖常?
使用命令clang -rewrite-objc block.m
生成中間層代碼:
從下往上看驯镊,最下面就是main
函數(shù),
//block1的聲明
int (*block1)() = ((int (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
main_block_impl_0
:就是block1
的內(nèi)部實現(xiàn)結(jié)構(gòu);
__main_block_func_0
:就是block1
對應(yīng)的函數(shù)執(zhí)行方法板惑;
其中橄镜,對變量a的使用是通過int a = __cself->a; // bound by copy
來實現(xiàn)的;
main_block_desc_0
:block1
的描述信息冯乘,main_block_impl_0
中由于增加了一個變量a洽胶,所以結(jié)構(gòu)體的大小變大了,該結(jié)構(gòu)體大小被寫在了main_block_desc_0中裆馒,Block_size
就是block1
的大小姊氓。
所以block引用外部變量,其實是在自己的結(jié)構(gòu)中生成了一個相同的內(nèi)部變量喷好,然后在block執(zhí)行的時候翔横,將block外部變量的值賦給內(nèi)部變量。
也就是說block
捕獲了這個變量(Capture local variable)
這樣就能理解梗搅,在 block 外部再去修改變量 a 的內(nèi)容禾唁,是不會影響內(nèi)部的實際變量 a 的值了。
如果此時我在block1
中去修改a的值會怎樣呢无切?
報錯了荡短,需要添加__block
修飾這個變量才可以,分析一下原因:
看看源碼哆键,可以知道變量a在main
函數(shù)中聲明掘托,所以當block1
被調(diào)用后,函數(shù)__main_block_func_0
已經(jīng)出了a的作用區(qū)域籍嘹,顯然是無法修改a的值的闪盔。
int a = __cself->a; // bound by copy
只是將a的值從main
中傳到__main_block_func_0
中,變量a的作用域還是沒有變噩峦。
這就就相當于你在一個函數(shù)中去訪問另外一個函數(shù)中的局部變量锭沟,當然時引用不到的。
當然识补,也可以通過變量的指針來修改變量的值族淮,但是當block被執(zhí)行的時候,引用的這個變量可能已經(jīng)不在棧中了凭涂,那么此時這個變量指針就成為了野指針祝辣,此時再訪問就會crash。
等等切油,既然是由于作用域的問題蝙斜,那么如果是全局變量的話,其作用域整個程序澎胡,不需要__block
會出現(xiàn)上面的問題嗎孕荠?
int b = 1;
int main(int argc, char * argv[]) {
__block int a = 1;
void (^block2)(void) = ^()
{
a = 2;
b = 2;
};
block2();
}
可以看到函數(shù)__main_block_func_0
是直接對變量b進行賦值修改的
-
修改外部變量
上面講到對變量用__block
來修飾之后娩鹉,就可以在block中修改這個變量,那么這中間發(fā)生了什么呢稚伍?
對下面代碼rewrite之后弯予,可以看到:
int main(int argc, char * argv[]) {
__block int a = 1;
void (^block2)(void) = ^()
{
a = 2;
};
block2();
}
可以看到多了一些結(jié)構(gòu):
__Block_byref_val_0
:其實就是__block
修飾的變量
其內(nèi)部有一個__forwarding
,是指向自己的个曙,在重新給變量a賦值的時候锈嫩,其實是給__Block_byref_val_0
中的a賦值。
簡單的說:
- __block修飾后的成員變量垦搬,其實是將這個成員變量包裝成了一個結(jié)構(gòu)體
__Block_byref_val_0
呼寸; - 在block捕獲變量為__block修飾的外部變量時,才會出現(xiàn)這兩個函數(shù):
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*/);}
3.調(diào)用__main_block_copy_0
會將__block類型的成員變量從棧上復(fù)制到堆上猴贰;而當block被釋放時对雪,相應(yīng)地會調(diào)用__main_block_dispose_0
來釋放__block類型的成員變量。
4.此時米绕,棧上和堆上都存在__block變量慌植,即使棧上的變量作用域結(jié)束,堆上的仍然可以使用义郑。
5.那么什么時候block會復(fù)制到堆上呢?
調(diào)用block的copy方法丈钙。
block作為函數(shù)返回值返回時非驮。
block調(diào)用外面的_strong的id的類時,或用_block時雏赦。
方法中劫笙,用usingblock或者GCD中的API時。
循環(huán)引用&內(nèi)存泄漏
了解了上述原因星岗,也就知道了為什么會產(chǎn)生循環(huán)引用的現(xiàn)象了:
對于引用外部變量的這種情況來說填大,block會在內(nèi)部結(jié)構(gòu)中生成一個相同的內(nèi)部變量,那么這在MRC下就會使該變量的retaincount加1俏橘,在ARC的情況下允华,就會持有該變量,形成一個強引用的狀態(tài)寥掐,那么如果這個變量是self靴寂,那么此時block和self之間就會出現(xiàn)你中有我,我中有你的情況召耘,也就是循環(huán)引用現(xiàn)象了百炬,如果這個變量是一個對象的話,就可能會因為無法釋放而出現(xiàn)內(nèi)存泄露污它。
那如何解決呢剖踊?
本質(zhì)上主要是破環(huán)這種你中有我庶弃,我中有你的關(guān)系,那么:
在ARC環(huán)境下德澈,可以用__weak來修改self歇攻,產(chǎn)生一個weakself,這樣圃验,當block外部的self釋放后掉伏,內(nèi)部引用的weakself就回自動釋放;
在MRC環(huán)境下澳窑,使用__block來修飾變量斧散。
本身無法避免循環(huán)引用的問題,但是我們可以通過在 block 內(nèi)部手動把 blockObj 賦值為 nil 的方式來避免循環(huán)引用的問題摊聋。另外一點就是 __block 修飾的變量在 block 內(nèi)外都是唯一的鸡捐,要注意這個特性可能帶來的隱患。
文中代碼見: demo
- 問題:
為什么系統(tǒng)的block麻裁,AFN網(wǎng)絡(luò)請求的block內(nèi)使用self不會造成循環(huán)引用?
1. UIView的動畫block不會造成循環(huán)引用的原因就是箍镜,這是個類方法,當前控制器不可能強引用一個類煎源,所以循環(huán)無法形成色迂。
2. AFN無循環(huán)是因為絕大部分情況下,你的網(wǎng)絡(luò)類對象是不會被當前控制器引用的手销,這時就不會形成引用環(huán)歇僧。
AFN中的block并沒有引用控制器對象。
參考:
http://blog.csdn.net/jasonblog/article/details/7756763
http://www.cocoachina.com/ios/20130802/6725.html
http://www.cocoachina.com/ios/20150106/10850.html
http://www.cocoachina.com/ios/20160307/15441.html
http://www.reibang.com/p/1383d56a7ca3
http://www.reibang.com/p/51d04b7639f1