Blocks的儲存域 Block和__block變數的實質就是 在棧上的結構體執行個體。 如:__block變數實質是 棧上__block變數的結構體執行個體。 其中Block也是oc的對象,該OC的類為:_NSConcreteStackBlock。 雖然該類並沒有出現在源碼,但是有很多與之類似的類: _NSConcreteStackBlock 它的對象Block設定在棧上 _NSConcreteGlobalBlock 他與全域變數一樣,設定在程式的資料區域(.data區)中。 _NSConcreteMallocBlock 它的對象設定在由malloc函數分配的記憶體塊中(堆)
前面我們看到的是stack,設定在棧上。 但是例如: void (^blk)(void) = ^{printf("sdfs");}; int main() ****
這裡初始化的時候用的是global: impl.isa = &_NSConcreteGlobalBlock;
此時,Block用結構體執行個體設定在程式的資料區域中。 因為在使用全域變數的地方不能使用自動變數,所以不存在對自動變數進行截獲。 Block用結構體執行個體的內容不依賴於執行時的狀態,所以整個程式中只需要一個執行個體。因此將Block用結構體執行個體設定在與全域變數相同的資料區域中即可。
只在截獲自動變數時,Block用結構體執行個體截獲的值才會根據執行時的狀態變化。 只要Block不截獲自動變數,就可以將Block用結構體執行個體設定在程式的資料區域。
在以下情況Block為_NSConcreteGlobalBlock類對象: 1、記述全域變數的地方有Block文法時 2、Block文法的運算式中不使用應截獲的自動變數時
除此之外的Block文法產生的Block為_NSConcreteStackBlock類對象,且設定在棧上。
還有就是設定在堆上的情況: 1、Block超出變數範圍可存在的原因 分配在棧上的Block和__block變數 其所屬的變數範圍結束,該Block或者__block變數也會被廢棄。 但是Blocks提供了 將Block和__block變數從棧上賦值到堆上的方法來解決這個問題, 這樣即使文法記述其範圍結束,堆上的Block也能繼續存在。
此時,賦值到堆上的Block將_NSConcreteMallocBlock類對象寫入Block用結構體執行個體的成員變數isa; impl.isa = _NSConcreteMallocBlock;
2、__block變數用結構體成員變數__forwarding存在的原因 上面的情況下只是Block,而__block變數需要用結構體成員變數__forwarding可以實現 無論__block變數配置在棧上還是堆上時都能夠正確地訪問__block變數。
在下面我們會說:在__block變數配置在堆上的狀態下,也可以訪問棧上的__block變數。 此時,只要棧上的結構體執行個體成員變數__forwarding指向堆上的結構體執行個體,不管是從棧上的__block變數還是從堆上的__block變數都能正確訪問。
Blocks提供的複製方法,如何從棧上複製到堆上的? ARC有效時候,編譯器可以自動判斷。 來看Block函數: typedef int (^blk_t)(int); blk_t func(int rate) { return ^(int count){return rate*count;}; } 此時將會返回配置在棧上的Block的函數。 即 程式執行中,從該函數返回函數調用方時變數範圍結束,因此棧上的Block也被廢棄。 雖然有問題,但是ARC的編譯如下: blk_t func(int rate) { blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate); tmp = objc_retainBlock(tmp); return objc_autoreleaseReturnValue(tmp); } 此時tmp為:blk_t __strong tmp。 這裡的objc_retainBlock實際上是:_Block_copy //將通過Block文法產生的Block, 即配置在棧上的Block用結構體執行個體, 賦值給相當於Block類型的變數tmp中 tmp = _Block_copy(tmp); //_Block_copy函數 將棧上的Block複製到堆上, 複製後, 將堆上的地址作為指標賦值給變數tmp return objc_autoreleaseReturnVlaue(tmp); //將堆上的Block作為OC對象, 註冊到autoreleasepool中,然後返回該對象。
將Block作為函數傳回值返回時,編譯器會自動產生複製到堆上的代碼。
但是 當我們使用copy執行個體方法的時候,我們需要手動產生代碼,將Block從棧上複製到堆上:alloc/new/copy/mutableCopy中的copy。 當我們向方法或者函數的參數中傳遞Block時 我們需要複製, 但是如果在方法或者函數中適當地複製了傳遞過來的參數,那麼就不需要在調用該方法或函數前手動複製了。 如: Cocoa架構的方法且方法名中含有usingBlock等。 Grand Central Dispatch的API。 例: - (id) getBlockArray { int val = 10; return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d", val);}, ^{NSLog("blk1:%d", val);}, nil]; }
id obj = getBlockArray(); typedef void (^blk_t)(void); blk_t blk = (blk_t)[obj objectAtIndex:0]; blk();
此時,blk()會發生錯誤,因為getBlockArray函數結束的時候,棧上的Block被廢棄。 需要如下: return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d", val);} copy], [^{NSLog(@"blk1:%d", val);} copy], nil];
這裡Block可以直接調用copy方法。 最後的blk = [blk copy];
對於配置在堆上的Block以及配置在程式的資料區域上的Block,調用copy會怎樣 ?
我們多次調用copy又會怎樣呢 ? 如:blk = [[[[blk copy] copy] copy] copy]; 解釋: { //配置在棧上的Block,賦值給變數blk中 blk_t tmp = [blk copy]; //將配置在堆上的Block賦值給變數tmp中,變數tmp持有強引用的Block blk = tmp; //將變數tmp 用Block賦值為變數blk, 變數blk持有強引用的Block。 //因為原先賦值的Block配置在棧上,所以不受此賦值的影響, 此時Block的持有人為變數blk和變數tmp } //由於變數範圍結束,所以變數tmp廢棄, 其強引用失效並釋放所持有的Block //由於Block被變數blk持有,所有沒有被廢棄。 { //配置在堆上的Block被賦值變數blk,同時變數blk持有強引用的Block blk_t tmp = [blk copy]; //配置在堆上的Block被賦值到變數tmp中,變數tmp持有強引用的Block blk = tmp; //由於變數blk進行了賦值,所以現在賦值的Block的強引用失效,Block被釋放。 //由於Block被變數tmp所持有,所以沒有被廢棄。 //變數blk中賦值了變數tmp的Block, 變數blk持有強引用的Block。 //此時Block的持有人為變數blk和變數tmp } //由於變數範圍結束,變數tmp被廢棄, 其強引用失效被釋放所持有的Block //由於變數blk還處於持有狀態,Block沒有被廢棄。 { blk_t tmp = [blk copy]; blk = tmp; } { blk_t tmp = [blk copy]; blk = tmp; }
__block變數儲存區 現在來說一下使用__block變數的Block從棧上複製到堆上會有什麼影響。
如果一個Block中使用__block變數,當該Block從棧賦值到堆時,使用的所有__block變數也必定配置在棧上。這些__block變數也全部被從棧複製到堆上。 此時Block持有__block變數。 即使在該Block已複製到堆的情況下,複製Block也對所使用的__block變數沒有任何影響。
當多個Block使用__block變數呢 ? 此時,第一個block複製到堆上後,剩下的Block複製到堆上時,被複製的Block也會持有__block變數,並增加__block變數的引用計數。
如果Block被廢棄,那麼它所使用的__block變數被釋放。
這裡的思考方式與OC的引用計數式記憶體管理完全相同。
現在來看一下__block變數用結構體成員變數__forwarding的原因: 不管__block變數配置在棧上還是在堆上,都能夠正確地訪問該變數。 也就是說,通過Block的複製,__block變數也從棧複製到了堆上,此時可同時訪問棧上的__block變數和堆上的__block變數: __block int val = 0; void (^blk)(void) = [^{++val;} copy]; ++val; blk(); NSLog(@"%d", val);
我們利用copy方法複製使用了__block變數的Block文法。 Block和__block變數兩者均是從棧上複製到堆上。 ^{++val;} //Block文法的運算式中使用初始化後的__block變數 堆上的__block變數 ++val; //Block文法之後使用與Block無關的變數。 複製前 棧上的__block變數
轉換: ++(val.__forwarding->val); 在棧上的__block變數用結構體執行個體在__block變數從棧複製到堆上時,會將成員變數__forwarding的值替換為複製目標堆上的__block變數用結構體執行個體的地址:
這樣,無論是在Block文法中,文法外使用__block變數,還是__block變數配置在棧或者堆上,都能順利訪問同一個__block變數。
截獲對象 blk_t blk; { id array = [[NSMutableArray alloc] init]; blk = [^(id obj) {[array addObject:obj];NSLog(@"array count = %ld", [array count]);} copy]; } blk ([[NSObject alloc] init]); blk ([[NSObject alloc] init]); blk ([[NSObject alloc] init]);
變數範圍結束,變數array被廢棄,強引用失效。 但是代碼運行正常, 這就說明賦值給變數array的NSMutableArray類的對象在該原始碼最後Block的執行部分超出其變數範圍而存在。 轉換後的源碼: //Block結構體,函數 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; id __strong array; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong_array, int flags = 0) : array(_array) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) { id __strong array = __cself->array; [array addObject:obj]; NSLog(@"array count = %ld", [array count]); }
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) { _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT); }
static void __main_block_desc_0 { unsigned long reserved; unsigned long Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0); __main_block_copy_0, __main_block_dispose_0 };
//Block文法,使用Block部分 blk_t blk; { id __strong array = [[NSMutableArray alloc] init]; blk = &__main_block_impl_0( __main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000); blk = [blk copy]; } (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
這裡要注意,被賦值NSMutableArray類對象並被截獲的自動變數array,可以看到他是Block用的結構體中附有__strong修飾符的成員: struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; id __strong array; }; 之前說:oc中,c結構體中不能含有附有__strong修飾符的變數,因為編譯器不知道何時進行c結構體的初始化和廢棄。 但是oc的執行階段程式庫可以準確把握Block從棧複製到堆以及堆上的Block被廢棄的時機。 所以 我們需要在__main_block_desc_0中增加copy和dispose,以及作為指標賦值給該成員變數的__main_block_copy_0函數和__main_block_dispose_0函數。
在原始碼的Block中,含有附有__strong修飾符的物件類型變數array,所以需要恰當管理賦值給變數array的對象,因此__main_block_copy_0函數使用_Block_object_assign函數將物件類型對象賦值給Block用結構體的成員變數array中並持有對象。 static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) { _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT); } _Block_object_assign 函數調用想讓與retain執行個體方法。 當然在dispose函數中也是這樣的。 _Block_object_dispose相當於 release執行個體方法。
我們來看看copy和dispose函數什麼時候被調用:
什麼時候棧上的Block賦值到堆? 1、調用Block的copy執行個體方法時。 2、Block作為函數傳回值返回時。 3、將Block賦值給附有__strong修飾符id類型的類或Block類型成員變數時。 4、在方法名中含有usingBlock的Cocoa架構方法或Grand Central Dispatch的API中傳遞Block時。
在調用Block的copy的時候,如果Block配置在棧上,那麼該Block會從棧複製到堆。 當Block作為函數傳回值的時候,將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變數的時候,編譯器會自動將對象的Block作為參數,並調用_Block_copy函數。這與調用Block的copy執行個體方法效果相同。在方法中含有usingBlock的Cocoa架構方法或Crand Central Dispatch的API中傳遞Block時,在該方法或函數內部對傳遞過來的Block調用Block的copy執行個體方法或者_Block_copy函數。 也就是說,雖然從原始碼來看,在上面這些情況下棧上的Block被複製到了堆上, 但其實可歸結為_Block_copy函數被調用時Block從棧複製到堆。 相反,釋放複製到堆上的Block後,誰都不持有Block而使其被廢棄時調用dispose函數, 相當於對象的dealloc執行個體方法。 這樣,通過使用附有__strong修飾符的自動變數,Block中截獲的對象就能超出其變數範圍存在了。
截獲對象和使用__block變數時的不同: 對象:BLOCK_FIELD_IS_OBJECT __block變數:BLOCK_FIELD_IS_BYREF 通過標誌來區分是對象還是__block變數。
但是與copy函數持有截獲的對象,dispose函數釋放截獲的對象相同,copy函數持有所使用的__block比那裡,dispose函數釋放所使用的__block變數。
因此,Block中使用的賦值給附有__strong修飾符的自動變數的對象和複製到堆上的__block變數由於被堆上的Block所持有,因而可超出其變數範圍而存在。
在Block中使用物件類型自動變數時,除了以下3種情況,推薦調用Block的copy執行個體方法。 1、Block作為函數傳回值返回時。 2、將Block賦值給類的附有__strong修飾符的id類型或Block類型成員變數時。 3、向方法名中含有usingBlock的Cocoa架構方法或Crand Central Dispatch的API中傳遞Block時。
-------2014/3/22 Beijing