標籤:
最近看了很多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¬eGuid=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的追探