說道block大家都不陌生,記憶體管理問題也是開發者最頭疼的問題,網上很多講block的部落格,但大都是理論性多點,今天結合一些執行個體來講解下。
存儲域
首先和大家聊聊block的存儲域,根據block在記憶體中的位置,block被分為三種類型:
- NSGlobalBlock
- NSStackBlock
- NSMallocBlock
從字面意思上大家也可以看出來
1、NSGlobalBlock是位于全局區的block,它是設定在程式的資料區域(.data區)中。
2、NSStackBlock是位于棧區,超出變量作用域,棧上的Block以及 ____block__變量都被銷毀。
3、NSMallocBlock是位于堆區,在變量作用域結束時不受影響。
注意:在 ARC 開啟的情況下,将隻會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 類型的 block。
作為一個開發者,有一個學習的氛圍跟一個交流圈子特别重要,這是一個我的iOS交流群:
413038000,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿裡面試題、面試經驗,讨論技術, 大家一起交流學習成長!
推薦閱讀: iOS開發——2020 最新 BAT面試題合集(持續更新中)
說了這麼多理論的東西,有些人可能很懵,覺得講這些有什麼用呢,我平時使用block并沒有什麼問題啊,好了,接下來我們先來個🌰感受下:
#import "ViewController.h"
void(^block)(void);
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSInteger i = 10;
block = ^{
NSLog(@"%ld", i);
};
}
- (void)touchesBegan:(NSSet<UITouch *> *)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的類 | 配置存儲域 | 複制效果 |
---|---|---|
_NSConcreteGlobalBlock | 程式資料區域 | 什麼也不做 |
_NSConcreteStackBlock | 棧 | 從棧複制到堆上 |
_NSConcreteMallocBlock | 堆 | 引用計數加增加 |
通過這張表我們可以清晰看到三種Block copy之後到底做了什麼,接下來我們就來分别看看這三種類型的Block。
在記述全局變量的地方使用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文法的表達式中沒有使用應截獲的自動變量時
配置在全局區的block,從變量作用域外也可以通過指針安全地使用。但是設定在棧上的block,如果其作用域結束,該block就被銷毀。同樣的,由于
__block
變量也配置在棧上,如果其作用域結束,則該
__block
變量也會被銷毀。
上面舉得例子其實就是生成在棧上的block:
NSInteger i = 10;
block = ^{
NSLog(@"%ld", i);
};
除了配置在程式資料區域的block(全局Block),其餘生成的block為_NSConcreteStackBlock類對象,且設定在棧上,那麼配置在堆上的__NSConcreteMallocBlock類何時使用呢?
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);
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底層源碼實作的講解,講的很透徹,分析的很好!