Block的那些事

看了很多別人寫的Block的相關(guān)文章,但是別人寫的終究是別人的,看過之后沒多久也就忘了。真正的動(dòng)手敲敲,仔細(xì)考究一下,然后用自己的思路總結(jié)出來,才會(huì)轉(zhuǎn)化為自己的東西,才會(huì)牢記于心,這也是我寫這篇文章的原因。

1. Block是什么

答:是匿名函數(shù),是閉包,是對(duì)象。

但是怎么理解呢?

匿名函數(shù):是說在定義Block時(shí),其名稱可以省略不寫;
閉包:封閉的包,呵呵,看Block寫法就知道了。
還有人理解為閉包就是能夠讀取其它函數(shù)內(nèi)部變量的函數(shù)
這中說法怎么理解呢?
就是閉包允許一個(gè)函數(shù)訪問聲明該函數(shù)運(yùn)行上下文中的變量,甚至可以訪問不同運(yùn)行上文中的變量。

**用腳本語言來解釋:**
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,這是為什么呢,因?yàn)殚]包性。

來自: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ù)中聲明了一個(gè)整型,并賦值42,另外還聲明了一個(gè)block,該block會(huì)將42返回。
 
然后將block傳遞給logBlock函數(shù),該函數(shù)會(huì)顯示出返回的值42。

即使是在函數(shù)logBlock中執(zhí)行block,而block又聲明在main函數(shù)中,但是block仍然可以訪問到x變量,并將這個(gè)值返回。

個(gè)人覺得這個(gè)例子很low,感覺只是個(gè)值傳遞的過程,并不能很好的解釋閉包。

來自:http://www.cocoachina.com/ios/20130715/6599.html

對(duì)象::我們都知道,Objective-C中的對(duì)象,其實(shí)是一個(gè)struct(結(jié)構(gòu)體),所有的對(duì)象的數(shù)據(jù)結(jié)構(gòu)里面都有一個(gè)isa指針,
那我們可以使用clang工具來看看Block的數(shù)據(jù)結(jié)構(gòu)是怎樣的:

新建一個(gè)block.m文件,里面的代碼:

int main(int argc, char * argv[]) {
    void (^block)(void) = ^{
//        NSLog(@"hello");
    };
}

在終端使用命令clang -rewrite-objc block.m之后,會(huì)產(chǎn)生一個(gè)block.cpp文件,其中__main_block_impl_0:就是block的實(shí)現(xiàn)源碼;

屏幕快照 2016-05-23 22.08.15.png

可以看到Block其實(shí)也是一個(gè)struct,內(nèi)部也有一個(gè)isa指針,指向這個(gè)Block的類型:NSConcreteStackBlock。

所以block也是對(duì)象

2. Block的寫法

  • 聲明一個(gè)block:
返回值(^ 名稱)(參數(shù)列表)= ^(參數(shù)列表){
  //balabala
};
  • block作為函數(shù)的參數(shù):
-(void)testAction:(返回值(^)(參數(shù)列表))名稱
  • 使用typedef來聲明一個(gè)全局的block變量
type 返回值 (^ 名稱)(參數(shù)列表)

3. Block的類型

我們都知道block有三種類型

__NSConcreteGlobalBlock__;
__NSConcreteStackBlock__; 
__NSConcreteMallocBlock__;

怎么理解這三種類型呢?

顧名思義:

__NSConcreteGlobalBlock__://存儲(chǔ)在全局?jǐn)?shù)據(jù)區(qū)域的block,不會(huì)訪問任何外部變量
__NSConcreteStackBlock__; //存儲(chǔ)在棧上的block,出棧時(shí)會(huì)被銷毀
__NSConcreteMallocBlock__;//存儲(chǔ)在堆上的block,當(dāng)引用計(jì)數(shù)為0時(shí)會(huì)被銷毀

站在代碼表現(xiàn)形式的角度來說:

  • 沒有引用外部變量的block就是__NSConcreteGlobalBlock__
  • 引用了外部變量的block就是__NSConcreteStackBlock__;
  • 對(duì)__NSConcreteStackBlock__進(jìn)行copyBlock_copy()操作后,就會(huì)變?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>**

當(dāng)然,也可以用命令clang -rewrite-objc block.m生成中間層代碼,看每一個(gè)block中的isa的值也可以得到block的類型。

注:在ARC環(huán)境下,是不存在__NSConcreteStackBlock__的,因?yàn)锳RC下,編譯器會(huì)隱式的對(duì)一些變量進(jìn)行copy操作,那原先的__NSConcreteStackBlock__就會(huì)變?yōu)?code>__NSConcreteMallocBlock__。

所以ARC環(huán)境下只會(huì)存在兩種block:
__NSConcreteGlobalBlock__
__NSConcreteMallocBlock__

4. Block引用/修改外部變量

  • 引用外部變量
    對(duì)于 block 外的變量引用,block 默認(rèn)是將其復(fù)制到其數(shù)據(jù)結(jié)構(gòu)中來實(shí)現(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在聲明的時(shí)候,變量a的值已經(jīng)被復(fù)制到block1中使用了,所以下一行雖然變量a的值改變了,但是block1中使用的那個(gè)值是不會(huì)受到影響的。

那么引用過程到底發(fā)生了什么呢?

使用命令clang -rewrite-objc block.m生成中間層代碼:

屏幕快照 2016-06-18 12.05.08.png

從下往上看,最下面就是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)部實(shí)現(xiàn)結(jié)構(gòu);
__main_block_func_0:就是block1對(duì)應(yīng)的函數(shù)執(zhí)行方法;
其中,對(duì)變量a的使用是通過int a = __cself->a; // bound by copy來實(shí)現(xiàn)的;
main_block_desc_0block1的描述信息,main_block_impl_0 中由于增加了一個(gè)變量a,所以結(jié)構(gòu)體的大小變大了,該結(jié)構(gòu)體大小被寫在了main_block_desc_0中,Block_size就是block1的大小。

所以block引用外部變量,其實(shí)是在自己的結(jié)構(gòu)中生成了一個(gè)相同的內(nèi)部變量,然后在block執(zhí)行的時(shí)候,將block外部變量的值賦給內(nèi)部變量。

也就是說block捕獲了這個(gè)變量(Capture local variable)

這樣就能理解,在 block 外部再去修改變量 a 的內(nèi)容,是不會(huì)影響內(nèi)部的實(shí)際變量 a 的值了。

如果此時(shí)我在block1中去修改a的值會(huì)怎樣呢?

屏幕快照 2016-06-18 12.19.16.png

報(bào)錯(cuò)了,需要添加__block修飾這個(gè)變量才可以,分析一下原因:

看看源碼,可以知道變量a在main函數(shù)中聲明,所以當(dāng)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的作用域還是沒有變。

這就就相當(dāng)于你在一個(gè)函數(shù)中去訪問另外一個(gè)函數(shù)中的局部變量,當(dāng)然時(shí)引用不到的。
當(dāng)然,也可以通過變量的指針來修改變量的值,但是當(dāng)block被執(zhí)行的時(shí)候,引用的這個(gè)變量可能已經(jīng)不在棧中了,那么此時(shí)這個(gè)變量指針就成為了野指針,此時(shí)再訪問就會(huì)crash。

等等,既然是由于作用域的問題,那么如果是全局變量的話,其作用域整個(gè)程序,不需要__block會(huì)出現(xiàn)上面的問題嗎?

int b = 1;
int main(int argc, char * argv[]) {
    __block int a = 1;
    void (^block2)(void) = ^()
    {
        a = 2;
        b = 2;
    };
    
    block2();
}
屏幕快照 2016-06-18 15.26.26.png

可以看到函數(shù)__main_block_func_0是直接對(duì)變量b進(jìn)行賦值修改的

  • 修改外部變量
    上面講到對(duì)變量用__block來修飾之后,就可以在block中修改這個(gè)變量,那么這中間發(fā)生了什么呢?

對(duì)下面代碼rewrite之后,可以看到:

    int main(int argc, char * argv[]) {
    __block int a = 1;
    void (^block2)(void) = ^()
    {
        a = 2;
    };
    
    block2();
}
屏幕快照 2016-06-18 12.10.41.png

可以看到多了一些結(jié)構(gòu):
__Block_byref_val_0:其實(shí)就是__block修飾的變量
其內(nèi)部有一個(gè)__forwarding,是指向自己的,在重新給變量a賦值的時(shí)候,其實(shí)是給__Block_byref_val_0中的a賦值。

簡單的說:

  1. __block修飾后的成員變量,其實(shí)是將這個(gè)成員變量包裝成了一個(gè)結(jié)構(gòu)體__Block_byref_val_0
  2. 在block捕獲變量為__block修飾的外部變量時(shí),才會(huì)出現(xiàn)這兩個(gè)函數(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會(huì)將__block類型的成員變量從棧上復(fù)制到堆上;而當(dāng)block被釋放時(shí),相應(yīng)地會(huì)調(diào)用__main_block_dispose_0來釋放_(tái)_block類型的成員變量。

4.此時(shí),棧上和堆上都存在__block變量,即使棧上的變量作用域結(jié)束,堆上的仍然可以使用。

5.那么什么時(shí)候block會(huì)復(fù)制到堆上呢?

  • 調(diào)用block的copy方法。

  • block作為函數(shù)返回值返回時(shí)。

  • block調(diào)用外面的_strong的id的類時(shí),或用_block時(shí)。

  • 方法中,用usingblock或者GCD中的API時(shí)。

  • 循環(huán)引用&內(nèi)存泄漏

了解了上述原因,也就知道了為什么會(huì)產(chǎn)生循環(huán)引用的現(xiàn)象了:

對(duì)于引用外部變量的這種情況來說,block會(huì)在內(nèi)部結(jié)構(gòu)中生成一個(gè)相同的內(nèi)部變量,那么這在MRC下就會(huì)使該變量的retaincount加1,在ARC的情況下,就會(huì)持有該變量,形成一個(gè)強(qiáng)引用的狀態(tài),那么如果這個(gè)變量是self,那么此時(shí)block和self之間就會(huì)出現(xiàn)你中有我,我中有你的情況,也就是循環(huán)引用現(xiàn)象了,如果這個(gè)變量是一個(gè)對(duì)象的話,就可能會(huì)因?yàn)闊o法釋放而出現(xiàn)內(nèi)存泄露。

那如何解決呢?
本質(zhì)上主要是破環(huán)這種你中有我,我中有你的關(guān)系,那么:
在ARC環(huán)境下,可以用__weak來修改self,產(chǎn)生一個(gè)weakself,這樣,當(dāng)block外部的self釋放后,內(nèi)部引用的weakself就回自動(dòng)釋放;

在MRC環(huán)境下,使用__block來修飾變量。
本身無法避免循環(huán)引用的問題,但是我們可以通過在 block 內(nèi)部手動(dòng)把 blockObj 賦值為 nil 的方式來避免循環(huán)引用的問題。另外一點(diǎn)就是 __block 修飾的變量在 block 內(nèi)外都是唯一的,要注意這個(gè)特性可能帶來的隱患。

文中代碼見: demo

  • 問題:
    為什么系統(tǒng)的block,AFN網(wǎng)絡(luò)請(qǐng)求的block內(nèi)使用self不會(huì)造成循環(huán)引用?
1. UIView的動(dòng)畫block不會(huì)造成循環(huán)引用的原因就是,這是個(gè)類方法,當(dāng)前控制器不可能強(qiáng)引用一個(gè)類,所以循環(huán)無法形成。

2. AFN無循環(huán)是因?yàn)榻^大部分情況下,你的網(wǎng)絡(luò)類對(duì)象是不會(huì)被當(dāng)前控制器引用的,這時(shí)就不會(huì)形成引用環(huán)。
AFN中的block并沒有引用控制器對(duì)象。

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.itdecent.cn/p/1383d56a7ca3
http://www.itdecent.cn/p/51d04b7639f1

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 《Objective-C高級(jí)編程》這本書就講了三個(gè)東西:自動(dòng)引用計(jì)數(shù)、block、GCD,偏向于從原理上對(duì)這些內(nèi)容...
    WeiHing閱讀 10,085評(píng)論 10 69
  • 前言 Blocks是C語言的擴(kuò)充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,852評(píng)論 0 23
  • 1: 什么是block?1.0: Block的語法1.1: block編譯轉(zhuǎn)換結(jié)構(gòu)1.2: block實(shí)際結(jié)構(gòu) 2...
    iYeso閱讀 946評(píng)論 0 5
  • Block 梳理與疑問 時(shí)隔一年,再次讀 《Objective-C 高級(jí)編程》,看到 block 一章,這一次從頭...
    DeerRun閱讀 749評(píng)論 0 2
  • 近來把《iOS與OS X多線程和內(nèi)存管理》這本書又掏出來看了一遍,這本書前前后后加起來看了能有三四遍了,每次看都有...
    老司機(jī)Wicky閱讀 2,416評(píng)論 5 46

友情鏈接更多精彩內(nèi)容