對Objective-C中Block的追探

來源:互聯網
上載者:User

標籤:

最近看了很多block相關的文章,都在說block怎麼用,寫的都很精彩。
blogs:
Block編程值得注意的那些事兒 (使用相關)
http://www.cocoachina.com/macdev/cocoa/2013/0527/6285.html
iOS中block實現的探究(內部結構分析)
http://blog.csdn.net/jasonblog/article/details/7756763?reload
還有緒斌同學共用的(內部結構分析)
https://www.evernote.com/shard/s269/sh/23b61393-c6dd-4fa2-b7ae-306e9e7c9639/131de66a3257122ba903b0799d36c04c?noteKey=131de66a3257122ba903b0799d36c04c&noteGuid=23b61393-c6dd-4fa2-b7ae-306e9e7c9639
又看了一本關於這方面的書:
Pro Multithreading and Memory Management for iOS and OS X
http://vdisk.weibo.com/s/9hjAV

覺得是可以總結一下我對block理解的時候了。
註:上面提供的資料有很多有用的背景知識比如block怎麼用,什麼時候應該加上__block關鍵字聲明變數,怎麼解決循環參考,什麼是堆,什麼是棧等等,大家寫的都比我好,我就不複製粘貼了。下面的文字是我的一些個人理解,如果有不對的地方還請指正。

1、block是個什嗎?
簡單說block就是一個“仿”對象。

在Objective-C中,類都是繼承NSObject的,NSObject裡面都會有個isa,是一個objc_class指標。
而block的對象,在clang的C++重寫中,

^int(){printf("val"); return 111;};

這個block會被轉化為

struct __block_impl {  void *isa;  int Flags;  int Reserved;  void *FuncPtr;};struct __testBlock_block_impl_0 {  struct __block_impl impl;  struct __testBlock_block_desc_0* Desc;  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {    impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};

__testBlock_block_impl_0是block結構,他的第一個屬性也是一個結構__block_impl,而第一個參數也是一個isa的指標。
在運行時,NSObject和block的isa指標都是指向在對象一個4位元組。
NSObject及衍生類別對象的isa指向Class的prototype,而block的isa指向了_NSConcreteStackBlock這個指標。
就是說當一個block被聲明的時候他都是一個_NSConcreteStackBlock類的對象。

2、block對象的生存期
通常在Objective-C中,對象都是在堆上聲明的。
當我們運行

NSString *str = [[NSString alloc] init];

的時候,這個NSString就在堆上掛上號了,直到release的時候,引用計數減為0,這個對象才會被幹掉。

再看一下block的內部實現,當我們實現

    {        void (^testBlock) (void) =  ^{printf("看看這個block");};        testBlock();    }

的時候,在clang中會被轉換為

    {        void (*testBlock) (void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);        ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);    }

 由此可見這個block是在棧上聲明的,這就是說當block超過了這個“}”,這個block對象就會被回收。

我們做個實驗:
Objective-C的源碼(非ARC)

    block stackBlock;    {        int val = 10;        stackBlock = ^{NSLog(@"val = %d", val);};    }    stackBlock();

轉換後:

    block stackBlock;    {        int val = 10;        stackBlock = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val);    }    ((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock);

上面的程式運行,不崩潰。
從這個轉換後的結果來看,__main_block_impl_0這個在棧上聲明的對象,在“}”結束應該就被釋放了,可是在下面的調用中居然還可以用?
我認為這就是運氣,個人不推薦在棧釋放block對象後再使用block對象。
看看__main_block_impl_0的聲明

struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;  int val;  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {    impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};

這個結構只做了建構函式,沒有做解構函式,導致在對象彈棧的時候沒有對對象內部變數賦值,所以飄在外面的地址都是野指標。
(註:clang這個命令不是全靠譜,只能作為參考,因為這個工具轉換出來的C++檔案無法通過編譯,感覺只能作為研究參考)

3、block外傳、賦值得上棧
咱們看看運氣不好的時候:
準備工作,弄個新的iOS工程(非ARC的),然後在ViewController.m裡定義一個blk類型。

typedef void (^blk) (void);

之後在弄個屬性

@interface ViewController () {      blk tempBlock;}@end

在viewDidLoad裡面加上一個按鈕,並聲明一個block指標付給tempBlock

- (void)viewDidLoad{    [superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.        UIButton *btn = [UIButton buttonWithType: UIButtonTypeRoundedRect];    btn.frame = CGRectMake(100.0f, 100.0f, 100.0f, 30.0f);    [btn setTitle: @"試試"forState: UIControlStateNormal];    [btn addTarget: self            action: @selector(btnClick:)  forControlEvents:UIControlEventTouchUpInside];    [self.viewaddSubview: btn];        __blockint val = 10;    tempBlock = ^{NSLog(@"val = %d", ++val);};}

按鈕點擊的事件:

- (void) btnClick: (id) Sender {    tempBlock();}

當頁面正常顯示之後,點擊按鈕必然崩潰。
提示error: address doesn‘t contain a section that points to a section in a object file
原因就是tempBlock所指向的對象已經被回收了。
在Objective-C中,這種情況也可能是tempBlock所指的對象被autorelease了?
這樣咱們吧tempBlock給retain一下不就好了嗎?

tempBlock = [^{NSLog(@"val = %d", ++val);} retain];

結果依舊,可問題出在哪呢?
我認為這就是block是一個“仿”對象的造成的,從之前的分析來看,block的isa指向的不是object_class,而是_NSConcreteStackBlock,我想這個prototype裡重新定義了咱們熟悉的retain/copy/release等NSObject所定義的函數。
retain這個函數在_NSConcreteStackBlock這個類的定義中,不會對指標做任何操作,所以才不會有影響。(同樣release也是一樣)

在block的各種使用說明中,都有一條,當block要作為參數外傳、賦值時都要調用copy,咱們對比一下copy前後block的變化。
把剛才的實驗改造一下:

- (void)viewDidLoad{    [superviewDidLoad];     //產生按鈕(略)        NSLog(@"_NSConcreteStackBlock %@", [_NSConcreteStackBlock class]);        __block int val = 10;    blk stackBlock = ^{NSLog(@"val = %d", ++val);};    NSLog(@"stackBlock: %@", stackBlock);        tempBlock = [stackBlock copy];    NSLog(@"tempBlock: %@", tempBlock);}

列印出的結果:

2013-05-29 14:21:09.969 BlockTest[2070:c07] _NSConcreteStackBlock __NSStackBlock__2013-05-29 14:21:09.970 BlockTest[2070:c07] stackBlock: <__NSStackBlock__: 0xbfffdb28>2013-05-29 14:21:09.970 BlockTest[2070:c07] tempBlock: <__NSMallocBlock__: 0x756bf20>

在經過copy之後,對象的類型從__NSStackBlock__變為了__NSMallocBlock__
在Objective-C的設計中,我沒見過copy一回把對象的類型也給變了的,再次說明了block是一種特殊的對象。
大家應該注意到__block標記的變數了吧,這個變數會隨著block對象上堆一塊上堆,這個部分上面的blogs和書中都有講解,我就不敘述了。
另外還有一種類型block的類型__NSGlobalBlock__,當block裡面沒有局部變數的時候會block會變為這個類型,這個類型的retain/copy/release全都不會對對象有影響,可以當做靜態block理解。
__NSMallocBlock__對象再次copy,不會再產生新的對象而是對原有對象進行retain。
經過實驗幾個block類型的retain/copy/release的功能如下(非ARC環境):


4、事情還沒完
當一個block對象上堆了,他的聲明周期就和一個普通的NSObject對象的方法一樣了(這個應該是__NSMallocBlock__這個類的設計參考了NSObject對象的設計)
作為一個合格的Objective-C程式員,見到copy應該就想到release。
在非ARC環境下,copy了block後一定要在使用後release,不然會有記憶體泄露,而且泄露點是在系統級,在Instruments裡跟不到問題觸發點,比較上火。


我在這裡想探討的另外一個問題是設計原則,對於一個對象,當外傳的時候我們都會想著把對象autorelease掉,比如:

- (NSArray *) myTestArray {    NSArray *array = [[NSArray alloc] initWithObjects: @"a", @"b", @"c", nil];    return [array autorelease];}

同樣,我們在向外傳遞block的時候一定也要做到,傳給外面一個在堆上的,autorelease的對象。

- (blk) myTestBlock {    __blockint val = 10;    blk stackBlock = ^{NSLog(@"val = %d", ++val);};    return [[stackBlock copy] autorelease];}

第一步,copy將block上從棧放到堆上,第二步,autorelease防止記憶體泄露。

同樣,有時我們會去將block放到別的類中做回調,如放到AFNetworking中的回調。
這時根據統一的設計原則,我們也應該給調用對象一個堆上的autorelease的對象。

總之,在把block對象外傳的時候,我們要傳出一個經過copy,再autorelease的block在堆上的__NSMallocBlock__對象。(個人觀點,block是模仿NSObject對象發明的,就不要讓調用方做與其他對象不一樣的事)

5、說說ARC
上面的這些方法,說的都是非ARC編程的時候的注意事項,在ARC下很多規則都可以省略了。
因為在ARC下有個原則,只要block在strong指標底下過一道都會放到堆上。
看下面這個實驗:

    {        __blockint val = 10;        __strong blk strongPointerBlock = ^{NSLog(@"val = %d", ++val);};        NSLog(@"strongPointerBlock: %@", strongPointerBlock); //1                __weak blk weakPointerBlock = ^{NSLog(@"val = %d", ++val);};        NSLog(@"weakPointerBlock: %@", weakPointerBlock); //2                NSLog(@"mallocBlock: %@", [weakPointerBlock copy]); //3                NSLog(@"test %@", ^{NSLog(@"val = %d", ++val);}); //4    }

得到的日誌

2013-05-29 16:03:58.773 BlockTest[3482:c07] strongPointerBlock: <__NSMallocBlock__: 0x7625120>2013-05-29 16:03:58.776 BlockTest[3482:c07] weakPointerBlock: <__NSStackBlock__: 0xbfffdb30>2013-05-29 16:03:58.776 BlockTest[3482:c07] mallocBlock: <__NSMallocBlock__: 0x714ce60>2013-05-29 16:03:58.777 BlockTest[3482:c07] test <__NSStackBlock__: 0xbfffdb18>

分析一下:
strong指標指向的block已經放到堆上了。
weak指標指向的block還在棧上(這種聲明方法只在block上有效,正常的weak指標指向堆上對象,直接就會變nil,需要用strong指標過一道,請參考ARC的指標使用注意事項)
第三行日誌同非ARC一樣,會將block從棧移動到堆上。
最後一行日誌,說明在單獨聲明block的時候,block還是會在棧上的。

在ARC下的另外一種情況,將block作為參數返回

- (__unsafe_unretained blk) blockTest {    int val = 11;    return ^{NSLog(@"val = %d", val);};}

調用方

NSLog(@"block return from function: %@", [self blockTest]);

得到的日誌:

2013-05-29 16:09:59.489 BlockTest[3597:c07] block return from function: <__NSMallocBlock__: 0x7685640>

分析一下:
在ARC環境下,當block作為參數返回的時候,block也會自動被移到堆上。

在ARC下,只要指標過一下strong指標,或者由函數返回都會把block移動到堆上。
所以在將block傳給回調方之前過一下strong指標,就可以滿足我剛才闡述的設計原則。

總結:
上面的文字介紹了:
1、block在Objective-C環境下的結構
     block是一個“仿”對象
2、block聲明的生存期
     棧上聲明對象是會被回收的,如果要長期持有block對象請把她移到堆上
3、從棧到堆的轉換時機
     棧上的block什麼時候會在執行copy的時候移動到堆上,block可以有三種類型
4、我個人理解的一些設計準則
     給調用方一個堆上的,被autorelease的block對象。
5、在ARC下的一些注意事項
     過一下strong指標,他好,我也好。

  Copyright ©2015 搖滾詩人

對Objective-C中Block的追探

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.