Block的那些事

看了很多別人寫的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)源碼讼积;

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

可以看到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__進行copyBlock_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生成中間層代碼:

屏幕快照 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)部實現(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_0block1的描述信息冯乘,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的值會怎樣呢无切?

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

報錯了荡短,需要添加__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();
}
屏幕快照 2016-06-18 15.26.26.png

可以看到函數(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();
}
屏幕快照 2016-06-18 12.10.41.png

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

簡單的說:

  1. __block修飾后的成員變量垦搬,其實是將這個成員變量包裝成了一個結(jié)構(gòu)體__Block_byref_val_0呼寸;
  2. 在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并沒有引用控制器對象。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锋拖,一起剝皮案震驚了整個濱河市诈悍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兽埃,老刑警劉巖侥钳,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異柄错,居然都是意外死亡舷夺,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門鄙陡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冕房,“玉大人,你說我怎么就攤上這事趁矾“也幔” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵毫捣,是天一觀的道長详拙。 經(jīng)常有香客問我帝际,道長,這世上最難降的妖魔是什么饶辙? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任蹲诀,我火速辦了婚禮,結(jié)果婚禮上弃揽,老公的妹妹穿的比我還像新娘脯爪。我一直安慰自己,他們只是感情好矿微,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布痕慢。 她就那樣靜靜地躺著,像睡著了一般涌矢。 火紅的嫁衣襯著肌膚如雪掖举。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天娜庇,我揣著相機與錄音塔次,去河邊找鬼。 笑死名秀,一個胖子當著我的面吹牛励负,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播匕得,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼熄守,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了耗跛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤攒发,失蹤者是張志新(化名)和其女友劉穎调塌,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惠猿,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡羔砾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了偶妖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姜凄。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖趾访,靈堂內(nèi)的尸體忽然破棺而出态秧,到底是詐尸還是另有隱情,我是刑警寧澤扼鞋,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布申鱼,位于F島的核電站愤诱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏捐友。R本人自食惡果不足惜淫半,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望匣砖。 院中可真熱鬧科吭,春花似錦、人聲如沸猴鲫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽变隔。三九已至规伐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間匣缘,已是汗流浹背猖闪。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肌厨,地道東北人培慌。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像柑爸,于是被迫代替她去往敵國和親吵护。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容

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