Block內(nèi)存管理實例分析

說道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ā)生以下錯誤:

Paste_Image.png

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

Paste_Image.png

解決辦法

解決辦法呢有兩種:

  • 一、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底層源碼實現(xiàn)的講解,講的很透徹,分析的很好!

希望可以通過上面的一些例子,可以讓大家加深對block的理解,知其然并且知其所以然。

如果您喜歡這篇文章,請動動手指點擊下方的喜歡!
您的支持將鼓勵我繼續(xù)創(chuàng)作!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容