前言 現在有一種說法,是開啟arc選項時,已經沒有棧上的block了,所以所有的block都不需要copy來拷貝到堆上了。那麼這個說法正確與否呢? 結論是這個說法必須是錯誤的,首先的一點就是arc只是編譯器幫助我們給對象自動增加retain,release方法,我們不需要手動的去管理這些成對出現的內 ...
前言
現在有一種說法,是開啟arc選項時,已經沒有棧上的block了,所以所有的block都不需要copy來拷貝到堆上了。那麼這個說法正確與否呢?
結論是這個說法必須是錯誤的,首先的一點就是arc只是編譯器幫助我們給對象自動增加retain,release方法,我們不需要手動的去管理這些成對出現的記憶體計數方法,其本質上與mrc是一脈相承的,所以arc下必然還是有stack block的。
再次可以簡單的寫一個例子(就舉參考1的要點3):
1 -(id) getBlockArray{
2 int val =10;
3 return [NSArray arrayWithObjects:
4 ^{NSLog(@"blk0:%d",val);},
5 ^{NSLog(@"blk1:%d",val);},nil];
6 }
7 // Other Method
8 id obj = getBlockArray();
9 typedef void (^blk_t)(void);
10 blk_t blk = (blk_t){obj objectAtIndex:0};
11 blk();
這段代碼會異常,但是作者解釋的不正確。首先我們可以列印一下這個Array,會發現第一個是NSMallocBlock,第二個是NSStackBlock。所以這段代碼說明瞭arc下也是有stack block的。其次這段代碼異常是因為array釋放的時候,第二個block是棧上面的,對其釋放必然會引發異常。
為什麼會這樣呢,我們接著往下看。
一、NSArray 的生成
1. + (instancetype)arrayWithObjects:(ObjectType)firstObj, ...
- (void)show
{
id obj = [self getBlockArray];
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();
blk = (blk_t)[obj objectAtIndex:1];
blk();
}
-(id) getBlockArray{
int val =10;
NSArray* a = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}, nil];
return a;
}
通過 clang rewrite 看看得到的c++代碼,關鍵地方如下:
static id _I_BlockTest_getBlockArray(BlockTest * self, SEL _cmd) {
int val =10;
NSArray* a = ((NSArray *(*)(id, SEL, ObjectType, ...))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("arrayWithObjects:"), (id)((void (*)())&__BlockTest__getBlockArray_block_impl_0((void *)__BlockTest__getBlockArray_block_func_0, &__BlockTest__getBlockArray_block_desc_0_DATA, val)), ((void (*)())&__BlockTest__getBlockArray_block_impl_1((void *)__BlockTest__getBlockArray_block_func_1, &__BlockTest__getBlockArray_block_desc_1_DATA, val)), __null);
return a;
}
第一個 block 強轉成 id 類型,第二個 block 沒有。首先 block 在賦值給 id 類型或者 block 類型的成員變數時,block 會拷貝到堆上,所以第一個 block 變成了堆上的 block,但是第二個還是棧上的記憶體。
整個 getBlockArray 方法在 show 方法中調用,所以用的是同一個 runloop 中帶過來的 autoreleasepool,getBlockArray 中的 NSArray* a = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}, nil];
會往這個 autoreleasepool 中添加兩個 NSArray (賦值給 a 是添加一次,show 中得到返回值時添加一次),這兩個NSArray 不會立即釋放,而會在 這個 runloop 結束的時候釋放,這個時機會在show的結束設置更外層的調用。而這個時機已經超過了 getBlockArray
的區域,超過這個區域去訪問棧記憶體,所以會crash。
2.一些其他的 NSArray 生成方法說明
+ (instancetype)arrayWithArray:(NSArray<ObjectType> *)array;
效果同上
- (instancetype)initWithArray:(NSArray<ObjectType> *)array;
效果同上
+ (instancetype)arrayWithObject:(ObjectType)anObject;
rewrite後代碼參考上面,這個方法的block會轉成 id,所以生成 NSArray 中的 block 是堆上的 block。
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;
不管flag 是 true 還是 false,都會crash
另外 - (void)addObject:(ObjectType)anObject;
也是因為會轉成 id,所以添加的都是堆上的 block。
二、AutoReleasePool
現在給 getBlockArray
加上一個autoreleasepool,看看會發生什麼
-(id) getBlockArray{
int val =10;
NSArray* a = nil;
@autoreleasepool {
a = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}, nil];
}
return [[NSArray alloc] initWithArray:a copyItems:YES];
}
此時並不會crash,正確的運行了,clang rewrite 一下看看
static id _I_BlockTest_getBlockArray(BlockTest * self, SEL _cmd) {
int val =10;
NSArray* a = __null;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
a = ((NSArray *(*)(id, SEL, ObjectType, ...))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("arrayWithObjects:"), (id)((void (*)())&__BlockTest__getBlockArray_block_impl_0((void *)__BlockTest__getBlockArray_block_func_0, &__BlockTest__getBlockArray_block_desc_0_DATA, val)), ((void (*)())&__BlockTest__getBlockArray_block_impl_1((void *)__BlockTest__getBlockArray_block_func_1, &__BlockTest__getBlockArray_block_desc_1_DATA, val)), __null);
}
return ((NSArray *(*)(id, SEL, NSArray<ObjectType> *, BOOL))(void *)objc_msgSend)((id)((NSArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("alloc")), sel_registerName("initWithArray:copyItems:"), (NSArray *)a, ((bool)1));
}
其實並沒有多出來什麼特殊的處理,我們可以分析分析。
getBlockArray
給 a 賦值的數組,加入到了我們添加的autoreleasepool中,在這個作用域外面,他已經被釋放了,其中的棧上的 block 在自己的作用域內釋放,沒有任何問題。而 a 預設是一個 strong 修飾的變數,在不在使用的時候,編譯器會幫我們添加上 release 而釋放,所以在函數結束的時候,他也釋放了。
另外 copyItem 是 YES,他會往 withArray 的每個 item 都發送 copy 消息,所以函數返回的 NSArray 中每個 item 都是堆上的 block,所以這一次並不會 crash。但是如果 copyItem 是 NO,那麼就會出現超過作用域訪問棧上 block 的問題,就會 crash 了。
三、字面量
還有一種生成 NSArray 的方式是:
NSArray* a = @[^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}];
看看對應的 rewrite 代碼是什麼樣的:
static id _I_BlockTest_getBlockArray(BlockTest * self, SEL _cmd) {
int val =10;
NSArray* a = __null;
a = ((NSArray *(*)(Class, SEL, const ObjectType *, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(2U, ((void (*)())&__BlockTest__getBlockArray_block_impl_0((void *)__BlockTest__getBlockArray_block_func_0, &__BlockTest__getBlockArray_block_desc_0_DATA, val)), ((void (*)())&__BlockTest__getBlockArray_block_impl_1((void *)__BlockTest__getBlockArray_block_func_1, &__BlockTest__getBlockArray_block_desc_1_DATA, val))).arr, 2U);
return a;
}
這裡我們看到有 __NSContainer_literal 這個函數,他做了些什麼呢?看下他的定義:
struct __NSContainer_literal {
void * *arr;
__NSContainer_literal (unsigned int count, ...) {
va_list marker;
va_start(marker, count);
arr = new void *[count];
for (unsigned i = 0; i < count; i++)
arr[i] = va_arg(marker, void *);
va_end( marker );
};
~__NSContainer_literal() {
delete[] arr;
}
};
多個棧上的 block 作為參數傳進去,然後在內部賦值給數組,當 block 作為返回值返回的時候,會被拷貝到堆上,所以,這個數組裡面的每個元素都是堆上的 block,所以不會crash。
四、NSDictionary
由於 NSDictionary 會 copy key 但是不 copy object,所以也會出現上面類似的crash情況。
參考1.http://blog.csdn.net/hherima/article/details/38620175
參考2.https://stackoverflow.com/questions/4010578/nsdictionary-dont-copy-values