說道block大家都不陌生,內(nèi)存管理問題也是開發(fā)者最頭疼的問題,網(wǎng)上很多講block的博客,但大都是理論性多點,今天結(jié)合一些實例來講解下。
存儲域
首先和大家聊聊block的存儲域,根據(jù)block在內(nèi)存中的位置,block被分為三種類型:
- NSGlobalBlock
- NSStackBlock
- NSMallocBlock
從字面意思上大家也可以看出來
1、NSGlobalBlock是位于全局區(qū)的block,它是設(shè)置在程序的數(shù)據(jù)區(qū)域(.data區(qū))中。
2、NSStackBlock是位于棧區(qū),超出變量作用域,棧上的Block以及 ____block__變量都被銷毀。
3、NSMallocBlock是位于堆區(qū),在變量作用域結(jié)束時不受影響。
注意:在 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<UITouch *> *)touches withEvent:(UIEvent *)event {
block();
}
@end
聲明這樣一個block,點擊屏幕的時候去調(diào)用這個block,然后就會發(fā)生以下錯誤:

野指針錯誤,顯而易見,這個是生成在棧上的block,因為超出了作用域而被釋放,所以再調(diào)用的時候報錯了,通過打印這個block我們也可以看到是生成在棧上的:

解決辦法
解決辦法呢有兩種:
- 一、Objective-C為塊常量的內(nèi)存管理提供了復(fù)制(Block_copy())和釋放(Block_release())命令。 使用Block_copy()命令可以將塊常量復(fù)制到堆中,這就像實現(xiàn)了一個將塊常量引用作為輸入?yún)?shù)并返回相同類型塊常量的函數(shù)。
- (void)viewDidLoad {
[super viewDidLoad];
NSInteger i = 10;
block = Block_copy(^{
NSLog(@"%ld", i);
});
}
為了避免內(nèi)存泄漏,Block_copy()必須與相應(yīng)的Block_release()命令達到平衡:
Block_release(block);
- 二、Foundation框架提供了處理塊的copy和release方法,這兩個方法擁有與Block_copy()和Block_release()函數(shù)相同的功能:
- (void)viewDidLoad {
[super viewDidLoad];
NSInteger i = 10;
block = [^{
NSLog(@"%ld", i);
} copy];
}
[block release];
到這里有人可能會有疑問了,為什么相同的代碼我建了一個工程,沒有調(diào)用copy,也沒有報錯啊,并且可以正確打印。 那是因為我們上面的操作都是在MRC下進行的,ARC下編譯器已經(jīng)默認執(zhí)行了copy操作,所以上面的這個例子就解釋了Block超出變量作用域可存在的原因。
接下來可能有人又要問了,block什么時候在全局區(qū),什么時候在棧上,什么時候又在堆上呢?上面的例子是對生成在棧上的Block作了copy操作,如果對另外兩種作copy操作,又是什么樣的情況呢?
| Block的類 | 配置存儲域 | 復(fù)制效果 |
|---|---|---|
| _NSConcreteGlobalBlock | 程序數(shù)據(jù)區(qū)域 | 什么也不做 |
| _NSConcreteStackBlock | 棧 | 從棧復(fù)制到堆上 |
| _NSConcreteMallocBlock | 堆 | 引用計數(shù)加增加 |
通過這張表我們可以清晰看到三種Block copy之后到底做了什么,接下來我們就來分別看看這三種類型的Block。
NSGlobalBlock
在記述全局變量的地方使用block語法時,生成的block為_NSConcreteGlobalBlock類對象
void(^block)(void) = ^ { NSLog(@"Global Block");};
int main() {
}
在代碼不截獲自動變量時,生成的block也是在全局區(qū):
int(^block)(int count) = ^(int count) {
return count;
};
block(2);
但是通過clang改寫的底層代碼指向的是棧區(qū):
impl.isa = &_NSConcreteStackBlock
這里引用巧神的一段話:由于 clang 改寫的具體實現(xiàn)方式和 LLVM 不太一樣,并且這里沒有開啟 ARC。所以這里我們看到 isa 指向的還是_NSConcreteStackBlock。但在 LLVM 的實現(xiàn)中,開啟 ARC 時,block 應(yīng)該是 _NSConcreteGlobalBlock 類型
總結(jié)下,生成在全局區(qū)block有兩種情況:
- 定義全局變量的地方有block語法時
- block語法的表達式中沒有使用應(yīng)截獲的自動變量時
NSStackBlock
配置在全局區(qū)的block,從變量作用域外也可以通過指針安全地使用。但是設(shè)置在棧上的block,如果其作用域結(jié)束,該block就被銷毀。同樣的,由于__block變量也配置在棧上,如果其作用域結(jié)束,則該__block變量也會被銷毀。
上面舉得例子其實就是生成在棧上的block:
NSInteger i = 10;
block = ^{
NSLog(@"%ld", i);
};
除了配置在程序數(shù)據(jù)區(qū)域的block(全局Block),其余生成的block為_NSConcreteStackBlock類對象,且設(shè)置在棧上,那么配置在堆上的__NSConcreteMallocBlock類何時使用呢?
NSMallocBlock
Blocks提供了將Block和__block變量從棧上復(fù)制到堆上的方法來解決這個問題,這樣即使變量作用域結(jié)束,堆上的Block依然存在。
impl.isa = &_NSConcreteMallocBlock;
這也是為什么Block超出變量作用域還可以存在的原因。
那么什么時候棧上的Block會復(fù)制到堆上呢?
- 調(diào)用Block的copy實例方法時
- Block作為函數(shù)返回值返回時
- 將Block賦值給附有__strong修飾符id類型的類或Block類型成員變量時
- 將方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時
上面只對Block進行了說明,其實在使用__block變量的Block從棧上復(fù)制到堆上時,__block變量也被從棧復(fù)制到堆上并被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執(zhí)行了copy操作,Block和__block變量均從棧復(fù)制到堆上。
然后在Block作用域之后我們又使用了與Block無關(guān)的變量:
++i;
一個是存在于棧上的變量,一個是復(fù)制到堆上的變量,我們是如何做到正確的訪問這個變量值的呢?
通過clang轉(zhuǎn)換下源碼來看下:
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 };
我們發(fā)現(xiàn)相比于沒有__block關(guān)鍵字修飾的變量,源碼中增加了一個名為 __Block_byref_i_0 的結(jié)構(gòu)體,用來保存我們要 capture 并且修改的變量 i。
在__Block_byref_i_0結(jié)構(gòu)體中我們可以看到成員變量__forwarding,它持有指向該實例自身的指針。那么為什么會有這個成員變量__forwarding呢?這也是正是問題的關(guān)鍵。
我們可以看到源碼中這樣一句:
++(i->__forwarding->i);
棧上的__block變量復(fù)制到堆上時,會將成員變量__forwarding的值替換為復(fù)制到堆上的__block變量用結(jié)構(gòu)體實例的地址。所以“不管__block變量配置在棧上還是堆上,都能夠正確的訪問該變量”,這也是成員變量__forwarding存在的理由。
循環(huán)引用
循環(huán)引用比較簡單,造成循環(huán)引用的原因無非就是對象和block相互強引用,造成誰都不能釋放,從而造成了內(nèi)存泄漏?;镜囊恍├游揖筒辉僦貜?fù)了,網(wǎng)上很多,也比較簡單,我就一個問題來討論下,也是開發(fā)中有人問過我的一個問題:
- ** block里面使用self會造成循環(huán)引用嗎? **
很顯然答案不都是,有些情況下是可以直接使用self的,比如調(diào)用系統(tǒng)的方法:
[UIView animateWithDuration:0.5 animations:^{
NSLog(@"%@", self);
}];
因為這個block存在于靜態(tài)方法中,雖然block對self強引用著,但是self卻不持有這個靜態(tài)方法,所以完全可以在block內(nèi)部使用self。
還有一種情況:
當block不是self的屬性時,self并不持有這個block,所以也不存在循環(huán)引用
void(^block)(void) = ^() {
NSLog(@"%@", self);
};
block();
只要我們抓住循環(huán)引用的本質(zhì),就不難理解這些東西。
希望可以通過上面的一些例子,可以讓大家加深對block的理解,知其然并且知其所以然。
如果您喜歡這篇文章,請動動手指點擊下方的喜歡!
您的支持將鼓勵我繼續(xù)創(chuàng)作!