說道block大家都不陌生,內存管理問題也是開發者最頭疼的問題,網上很多講block的博客,但大都是理論性多點,今天結合一些實例來講解下。
存儲域
首先和大家聊聊block的存儲域,根據block在內存中的位置,block被分為三種類型:
NSGlobalBlock
NSStackBlock
NSMallocBlock
從字面意思上大家也可以看出來
NSGlobalBlock是位於全局區的block,它是設置在程序的數據區域(.data區)中。
NSStackBlock是位於棧區,超出變量作用域,棧上的Block以及 __block變量都被銷毀。
NSMallocBlock是位於堆區,在變量作用域結束時不受影響。
注意:在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 類型的 block。
說了這麼多理論的東西,有些人可能很懵,覺得講這些有什麼用呢,我平時使用block並沒有什麼問題啊,好了,接下來我們先來個感受下:
#import "ViewController.h" void(^block)(void); @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSInteger i = 10; block = ^{ NSLog(@"%ld", i); }; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { block(); } @end
聲明這樣一個block,點擊屏幕的時候去調用這個block,然後就會發生以下錯誤:
野指針錯誤,顯而易見,這個是生成在棧上的block,因為超出了作用域而被釋放,所以再調用的時候報錯了,通過打印這個block我們也可以看到是生成在棧上的:
解決辦法
解決辦法呢有兩種:
Objective-C為塊常量的內存管理提供了復制(Block_copy())和釋放(Block_release())命令。 使用Block_copy()命令可以將塊常量復制到堆中,這就像實現了一個將塊常量引用作為輸入參數並返回相同類型塊常量的函數。
- (void)viewDidLoad { [super viewDidLoad]; NSInteger i = 10; block = Block_copy(^{ NSLog(@"%ld", i); }); }
為了避免內存洩漏,Block_copy()必須與相應的Block_release()命令達到平衡:
Block_release(block);
Foundation框架提供了處理塊的copy和release方法,這兩個方法擁有與Block_copy()和Block_release()函數相同的功能:
- (void)viewDidLoad { [super viewDidLoad]; NSInteger i = 10; block = [^{ NSLog(@"%ld", i); } copy]; }
[block release];
到這裡有人可能會有疑問了,為什麼相同的代碼我建了一個工程,沒有調用copy,也沒有報錯啊,並且可以正確打印。 那是因為我們上面的操作都是在MRC下進行的,ARC下編譯器已經默認執行了copy操作,所以上面的這個例子就解釋了Block超出變量作用域可存在的原因。
接下來可能有人又要問了,block什麼時候在全局區,什麼時候在棧上,什麼時候又在堆上呢?上面的例子是對生成在棧上的Block作了copy操作,如果對另外兩種作copy操作,又是什麼樣的情況呢?
通過這張表我們可以清晰看到三種Block copy之後到底做了什麼,接下來我們就來分別看看這三種類型的Block。
NSGlobalBlock
在記述全局變量的地方使用block語法時,生成的block為_NSConcreteGlobalBlock類對象
void(^block)(void) = ^ { NSLog(@"Global Block");}; int main() { }
在代碼不截獲自動變量時,生成的block也是在全局區:
int(^block)(int count) = ^(int count) { return count; }; block(2);
但是通過clang改寫的底層代碼指向的是棧區:
impl.isa = &_NSConcreteStackBlock
這裡引用巧神的一段話:由於 clang 改寫的具體實現方式和 LLVM 不太一樣,並且這裡沒有開啟 ARC。所以這裡我們看到 isa 指向的還是_NSConcreteStackBlock。但在 LLVM 的實現中,開啟 ARC 時,block 應該是 _NSConcreteGlobalBlock 類型
總結下,生成在全局區block有兩種情況:
定義全局變量的地方有block語法時
block語法的表達式中沒有使用應截獲的自動變量時
NSStackBlock
配置在全局區的block,從變量作用域外也可以通過指針安全地使用。但是設置在棧上的block,如果其作用域結束,該block就被銷毀。同樣的,由於__block變量也配置在棧上,如果其作用域結束,則該__block變量也會被銷毀。
上面舉得例子其實就是生成在棧上的block:
NSInteger i = 10; block = ^{ NSLog(@"%ld", i); };
除了配置在程序數據區域的block(全局Block),其余生成的block為_NSConcreteStackBlock類對象,且設置在棧上,那麼配置在堆上的__NSConcreteMallocBlock類何時使用呢?
NSMallocBlock
Blocks提供了將Block和__block變量從棧上復制到堆上的方法來解決這個問題,這樣即使變量作用域結束,堆上的Block依然存在。
impl.isa = &_NSConcreteMallocBlock;
這也是為什麼Block超出變量作用域還可以存在的原因。
那麼什麼時候棧上的Block會復制到堆上呢?
調用Block的copy實例方法時
Block作為函數返回值返回時
將Block賦值給附有__strong修飾符id類型的類或Block類型成員變量時
將方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時
上面只對Block進行了說明,其實在使用__block變量的Block從棧上復制到堆上時,__block變量也被從棧復制到堆上並被Block所持有。
接下來我們再來看一個????:
void(^block)(void); 99 int main(int argc, const char * argv[]) { @autoreleasepool { __block NSInteger i = 10; block = [^{ ++i; } copy]; ++i; block(); NSLog(@"%ld", i); } return 0; }
我們對這個生成在棧上的block執行了copy操作,Block和__block變量均從棧復制到堆上。
然後在Block作用域之後我們又使用了與Block無關的變量:
++i;
一個是存在於棧上的變量,一個是復制到堆上的變量,我們是如何做到正確的訪問這個變量值的呢?
通過clang轉換下源碼來看下:
void(*block)(void); struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; NSInteger i; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_i_0 *i; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_i_0 *i = __cself->i; // bound by ref ++(i->__forwarding->i); } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} static struct __main_block_desc_0 { size_t reserved; size_t 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}; int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 10}; block = (void (*)())((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344)), sel_registerName("copy")); ++(i.__forwarding->i); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); NSLog((NSString *)&__NSConstantStringImpl__var_folders_47_s4m8c9pj5mg0k9mymsm7rbmw0000gn_T_main_e69554_mi_0, (i.__forwarding->i)); } return 0; }
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
我們發現相比於沒有__block關鍵字修飾的變量,源碼中增加了一個名為 __Block_byref_i_0 的結構體,用來保存我們要 capture 並且修改的變量 i。
在__Block_byref_i_0結構體中我們可以看到成員變量__forwarding,它持有指向該實例自身的指針。那麼為什麼會有這個成員變量__forwarding呢?這也是正是問題的關鍵。
我們可以看到源碼中這樣一句:
++(i->__forwarding->i);
棧上的__block變量復制到堆上時,會將成員變量__forwarding的值替換為復制到堆上的__block變量用結構體實例的地址。所以“不管__block變量配置在棧上還是堆上,都能夠正確的訪問該變量”,這也是成員變量__forwarding存在的理由。
循環引用
循環引用比較簡單,造成循環引用的原因無非就是對象和block相互強引用,造成誰都不能釋放,從而造成了內存洩漏。基本的一些例子我就不再重復了,網上很多,也比較簡單,我就一個問題來討論下,也是開發中有人問過我的一個問題:
block裡面使用self會造成循環引用嗎?
很顯然答案不都是,有些情況下是可以直接使用self的,比如調用系統的方法:
[UIView animateWithDuration:0.5 animations:^{ NSLog(@"%@", self); }];
因為這個block存在於靜態方法中,雖然block對self強引用著,但是self卻不持有這個靜態方法,所以完全可以在block內部使用self。
還有一種情況:
當block不是self的屬性時,self並不持有這個block,所以也不存在循環引用
void(^block)(void) = ^() { NSLog(@"%@", self); }; block();
只要我們抓住循環引用的本質,就不難理解這些東西。
最後附上巧神對Block底層源碼實現的講解,講的很透徹,分析的很好!
希望可以通過上面的一些例子,可以讓大家加深對block的理解,知其然並且知其所以然。