iOS Block原理探究以及循環引用的問題

《Objective-C高級編程》這本書就講了三個東西:自動引用計數、block、GCD,偏向于從原理上對這些內容進行講解而且涉及到一些比較底層的實現,再加上因為中文翻譯以及內容條理性等方面的原因,書本有些內容比較晦澀難懂,在初初讀的時候一臉懵逼。本文是對書中block一章的內容做的一些筆記,所以側重的是講原理,同時也會對書中講得晦澀或不合理的地方相對進行一些補充和擴展。

1.Block結構與實質

使用Block的時候,編譯器對Block做了怎樣的轉換?
分析工具clang
例1

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    void (^blk)(void) = ^{
        NSLog(@"hello");
    };
    blk();
    
    return 0;
}

clang:

//block實現結構體
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

//block結構體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//block代碼塊中的實現
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_f871c6_mi_0);
    }

//block描述結構體
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
//block實現
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//block調用
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

從main函數入手,對應OC的代碼,里面一共做了兩件事:實現block、調用block。

1.實現block

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

它調用了__main_block_impl_0結構體的構造函數來實現。__main_block_impl_0結構體有兩個成員變量,分別是__block_impl結構體和__main_block_desc_0結構體。

// impl結構體
struct __block_impl {
  void *isa;  // 存儲位置,_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;  // 按位表示一些 block 的附加信息
  int Reserved;  // 保留變量
  void *FuncPtr;  // 函數指針,指向 Block 要執行的函數,即__main_block_func_0
};

// Desc結構體
static struct __main_block_desc_0 {
  size_t reserved;  // 結構體信息保留字段
  size_t Block_size;  // 結構體大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

再來看__main_block_impl_0結構體的構造函數

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

第一個參數需要傳入一個函數指針,第二個參數是作為靜態全局變量初始化的__main_block_desc_0結構體實例指針,第三個參數flags有默認值0。重點看第一個參數,實際調用中傳入的是__main_block_func_0函數指針:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_f871c6_mi_0);
    }

這個函數對應的實際上就是block中{}塊中的內容,通過block使用的匿名函數實際上被作為簡單的c語言函數來處理。這個函數的參數__cself就相當于OC里的self,__cself是__main_block_impl_0結構體指針。

總結:

void (^blk)(void) = ^{
        NSLog(@"hello");
    };

clang:
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

實現block,實際就是在方法中聲明一個結構體,并且初始化該結構體的成員。
將block語法生成的block賦值給block類型的變量blk,等同于將__main_block_impl_0結構體實例的指針賦給變量blk。

2.調用block

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

調用block就相對簡單多了。將第一步生成的block作為參數傳入FucPtr(也即_main_block_func_0函數),就能訪問block實現位置的上下文。

自此,block結構總體上分析完了,上面的c代碼看起來很復雜,但仔細讀的話還是很好理解的。
關于block的數據結構runtime是開源的。block的數據結構:

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};
 
struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

block結構

圖片來源。這張圖有幾個要說明的地方:
variables:block捕獲的變量,block 能夠訪問它外部的局部變量,就是因為將這些變量(或變量的地址)復制到了結構體中。這部分接下來會寫到。
而對于copy和dispose的部分,之后也會談到。

在objc中,根據對象的定義,凡是首地址是isa的結構體指針,都可以認為是對象(id)。這樣在objc中,block實際上就算是對象。

2.截獲外部變量

外部變量有四種類型:自動變量、靜態變量、靜態全局變量、全局變量。我們知道,如果不使用__block 就無法在block中修改自動變量的值。
那么block是怎么截獲外部變量的呢?測試代碼:
例2:

int a = 1;
static int b = 2;

int main(int argc, const char * argv[]) {

    int c = 3;
    static int d = 4;
    NSMutableString *str = [[NSMutableString alloc]initWithString:@"hello"];
    void (^blk)(void) = ^{
        a++;
        b++;
        d++;
        [str appendString:@"world"];
        NSLog(@"1----------- a = %d,b = %d,c = %d,d = %d,str = %@",a,b,c,d,str);
    };
    
    a++;
    b++;
    c++;
    d++;
str = [[NSMutableString alloc]initWithString:@"haha"];
    NSLog(@"2----------- a = %d,b = %d,c = %d,d = %d,str = %@",a,b,c,d,str);
    blk();
    
    return 0;
}

運行結果:

 2----------- a = 2,b = 3,c = 4,d = 5,str = haha
 1----------- a = 3,b = 4,c = 3,d = 6,str = helloworld

clang轉換之后:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

int a = 1;
static int b = 2;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *d;
  NSMutableString *str;
  int c;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_d, NSMutableString *_str, int _c, int flags=0) : d(_d), str(_str), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *d = __cself->d; // bound by copy
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy

        a++;
        b++;
        (*d)++;
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_2,a,b,c,(*d),str);
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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[]) {
    int c = 3;
    static int d = 4;
    NSMutableString *str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_0);
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &d, str, c, 570425344));

    a++;
    b++;
    c++;
    d++;
    str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_3);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_4,a,b,c,d,str);
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}
為了區別block實現前后棧上變量的變化,用棧1、棧2來做區別

變量a、b是全局的,它們在全局區。變量c、str在函數棧上,為了區別在block實現前、后函數棧上的變量,下文會用“棧1”、“棧2”來區別。

1.自動變量、靜態變量。
在__main_block_impl_0結構體中可以看到,成員變量多了:

int *d;
  NSMutableString *str;
  int c;

這也是為什么說block會截獲變量。接著看到構造函數:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_d, NSMutableString *_str, int _c, int flags=0) : d(_d), str(_str), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

構造函數中多了int *_d, NSMutableString *_str, int _c三個參數,并對對應結構體成員變量進行初始化。自此,自動變量和靜態變量被截獲為成員變量。

截獲變量的時機:
在main函數的實現中,

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &d, str, c, 570425344));

在實現block時,會將棧1參數傳入構造函數中進行初始化,所以,block會在實現的地方截獲變量,而截獲的變量的值也是實現時刻的變量值。另外,如果block語法表達式中沒有使用到的靜態變量、自動變量是不會被追加到__main_block_impl_0結構體中的。

然后我們來看一下這個問題:為什么在block語法表達式中不能改變自動變量的值,而靜態變量卻可以呢?從運行結果來看,為什么block內打印的自動變量的值沒有變化?

看到__main_block_func_0函數的實現:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *d = __cself->d; // bound by copy
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy

        a++;
        b++;
        (*d)++;
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_b870bb_mi_1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_b870bb_mi_2,a,b,c,(*d),str);
    }

為了便于下文的理解,我會把以下“=”左邊的變量,稱為“臨時變量”。

  int *d = __cself->d; // bound by copy
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy
  • 自動變量
    測試代碼中的自動變量有兩種:1、基本類型的自動變量 int c,2、指向對象的指針的自動變量 NSMutableString *str。有一個概念要強調,指針的值是地址。
  struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *d;
  NSMutableString *str;
  int c;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_d, NSMutableString *_str, int _c, int flags=0) : d(_d), str(_str), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

分析各種變量之間的關系:
block截獲自動變量為結構體成員變量,對應的數據類型是一樣的。
1、在實現block,調用__main_block_impl_0構造函數時,棧1自動變量的瞬時值就被截獲、復制保存到結構體成員變量中初始化。成員變量c得到的是自動變量c的值3,成員變量str得到的是自動變量str的值(可變字符串對象1的地址)。
2、在實現block后、調用block前,即棧2修改自動變量的值,對結構體中存儲的成員變量的值不會造成影響。此時,自動變量c的值為4,str的值為可變字符串對象2的地址。
3、調用block時,即調用__main_block_func_0函數,此時函數中臨時變量c、str取到的值是結構體中成員變量存儲的值,也即是3和可變字符串對象1的地址。

如果在block內修改自動變量的值是可行的,也就相當于是在__main_block_func_0函數中通過修改臨時變量的值,來達到修改棧上自動變量的值的目的。但根據上面分析,每一步都是值傳遞,所以棧上的自動變量的值修改和__main_block_func_0函數中修改臨時變量的值互不影響。
OC可能就是基于這一點,在編譯層面就防止開發者犯錯,因此如果在block中修改自動變量的值就會報錯!

如果在block內修改自動變量的值,那代碼應該是這樣的:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy
  
  c++;
  str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_3);
    }

雖然在block內不能修改str的值,即重新指向其他地址,比如str = [[NSMutableString alloc]init];,但可以在block內對str進行操作,比如[str appendString:@"world"];

結論:block在實現時捕獲自動變量的瞬時值。

總結:block捕獲到的變量,都是賦值給block的結構體的,相當于const不可改。可以這樣理解block內c和str都是const類型。str理解成是常量指針,所以不能修改它指向其他對象但可以修改它所指向對象的“值”。

  • 靜態變量
    從結構體成員變量int *d;看出,block截獲靜態變量為結構體成員變量,截獲的是靜態變量的指針(不是值傳遞了!)。
    調用block時,即調用__main_block_func_0函數,此時函數中臨時變量d取到的值是結構體中成員變量存儲的值,即指針,int *d = __cself->d;
    這看起來似乎和 自動變量是指向對象的指針 的情況差不多,但一點不同的是,在block內修改靜態變量的值是通過修改指針所指變量的來做的:(*d)++。而這也是為什么block內能修改自動變量的原因。

2.靜態全局變量、全局變量。從運行結果來看,這兩種外部變量的值都在block內、外得到增加。因為他們是全局的,作用域很廣,所以在block內、外都可以訪問得到它們。因為這兩種變量都沒有被追加到__main_block_impl_0結構體中成為成員變量,所以我覺得它們不算是被捕獲。

分析到這里,相信上面測試代碼為什么會得出這樣的運行結果應該也能理解了吧?

 2----------- a = 2,b = 3,c = 4,d = 5,str = haha
 1----------- a = 3,b = 4,c = 3,d = 6,str = helloworld

總結:
1.自動變量(基本數據類型變量、對象類型的指針變量),可以被block捕獲,但捕獲的是自動變量的值。不能在block內部改變自動變量的值。
2.靜態變量,可以被block捕獲,捕獲的是變量的地址。通過使用靜態變量的指針對其進行訪問,可以在block內改變值。
3.在block內沒有被使用到的自動變量、靜態變量不會被捕獲。
4.全局變量、靜態全局變量,因為作用域范圍廣,所以可以在block內改變它們的值。

現在來思考一個問題:
靜態變量可以在block里面直接改變值是通過傳遞內存地址值來實現的。那么為什么自動變量沒有使用這種方法呢?
下面看一個例子:例3

void(^blk_t)();
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
        int i = 1;
        int *a = &i;
        static int j = 2;
        blk_t = ^{
            (*a)++;
            NSLog(@"%d", *a);
        };
    blk_t();
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    blk_t();
}

ARC下運行結果:

2,2
1073741825,2 //點擊

這段代碼說明,變量作用域結束時,該作用域棧上的自動變量就被釋放了,因此,不能通過指針訪問原來的自動變量。棧上的變量被釋放掉了,因此點擊屏幕時訪問釋放掉的變量就會得到意想不到的值。
比如很多時候,block是作為參數傳遞供以后回調用的。往往回調時,定義變量所在的函數棧已經展開了,局部變量已經不再棧中了。

插一個題外話:

void(^blk_t)();
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    {
        int i = 1;
        int *a = &i;
 
        blk_t = ^{
            (*a)++;
            NSLog(@"%d", *a);
        };
    }
    blk_t();
}

本來例3這段代碼是想這樣寫的,但運行結果很正常。一度很疑惑,以為調用block時棧變量沒有釋放掉。但實際上它已經釋放了,只是它原來所占的地址還沒重新被分配給別的變量用,數據還是保持原來的。棧上占用的空間什么時候被釋放
例3的代碼會跑出這樣的結果,猜測和runloop休眠、喚醒之間釋放自動釋放池有關。

3.block的存儲域以及內存管理

3.1存儲域

一般,block有三種:_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock,根據Block對象創建時所處數據區不同而進行區別。

_NSConcreteGlobalBlock

是設置在程序的全局數據區域(.data區)中的Block對象。在全局聲明實現的block 或者 沒有用到自動變量的block為_NSConcreteGlobalBlock,生命周期從創建到應用程序結束。

  • 全局block:

    void (^glo_blk)(void) = ^{
        NSLog(@"global");
    };
    
    int main(int argc, const char * argv[]) {
        glo_blk();
        NSLog(@"%@",[glo_blk class]);
    }
    

    運行結果:

    global
    __NSGlobalBlock__
    

    同時,clang編譯后isa指針為_NSConcreteGlobalBlock。

  • 在函數棧上創建但沒有截獲自動變量

    int glo_a = 1;
    static int sglo_b =2;
    int main(int argc, const char * argv[]) {
        void (^glo_blk1)(void) = ^{//沒有使用任何外部變量
            NSLog(@"glo_blk1");
        };
        glo_blk1();
        NSLog(@"glo_blk1 : %@",[glo_blk1 class]);
        
        static int c = 3;
        void(^glo_blk2)(void) = ^() {//只用到了靜態變量、全局變量、靜態全局變量
            NSLog(@"glo_a = %d,sglo_b = %d,c = %d",glo_a,sglo_b,c);
        };
        glo_blk2();
        NSLog(@"glo_blk2 : %@",[glo_blk2 class]);
    

    運行結果:

    glo_blk1
    glo_blk1 : __NSGlobalBlock__
    glo_a = 1,sglo_b = 2,c = 3
    glo_blk2 : __NSGlobalBlock__
    

    然而,從clang編譯結果來看,這兩個block的isa的指針值都是_NSConcreteStackBlock。

_NSConcreteStackBlock和_NSConcreteMallocBlock

_NSConcreteStackBlock是設置在棧上的block對象,生命周期由系統控制的,一旦所屬作用域結束,就被系統銷毀了。
_NSConcreteMallocBlock是設置在堆上的block對象,生命周期由程序員控制的。
稍微改動一下例3的代碼:

void(^blk_t)();
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    int i = 1;
    int *a = &i;
    blk_t = ^{
        (*a)++;
        NSLog(@"%d", *a );
    };
    NSLog(@"%@",[blk_t class]);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    blk_t();
}

運行結果:
ARC:

__NSMallocBlock__
2017-08-11 23:45:52.513 RACPROJECT[49348:1786654] 1073741825

MRC:


運行結果會根據ARC\MRC環境而有所不同。
1.block的類型在ARC下是_NSConcreteMallocBlock,而在MRC下是_NSConcreteStackBlock。在ARC有效時,大多數情況下編譯器會恰當地判斷,自動生成將block從棧上復制到堆上的代碼。
2.在MRC下,由于Block是_NSConcreteStackBlock類型,它是存在于該函數的棧幀上的。當函數返回時,函數的棧幀被銷毀,這個block的內存也會被清除。因此在點擊屏幕時,程序如圖出現crash。
所以在函數結束后仍然需要這個block時,就必須用copy實例方法將它拷貝到堆上。這樣即使Block作用域結束,堆上的Block還可以繼續使用。

- (void)viewDidLoad {
    [super viewDidLoad];
    int i = 1;
    int *a = &i;
    blk_t = [^{
        (*a)++;
        NSLog(@"%d", *a );
    } copy];
    NSLog(@"%@",[blk_t class]);
}

MRC運行結果:

__NSMallocBlock__

3.2block的自動拷貝和手動拷貝

在ARC有效時,大多數情況下編譯器會進行判斷,自動生成將Block從棧上復制到堆上的代碼,以下幾種情況棧上的Block會自動復制到堆上:

  • 調用Block的copy方法
  • 將Block作為函數返回值時
  • 將Block賦值給__strong修飾的變量或Block類型成員變量時
  • 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時

因此ARC環境下多見的是MallocBlock,但StackBlock也是存在的:
不要進行任何copy、賦值等等操作,直接使用block

int main(int argc, const char * argv[]) {
    int val = 1;
    NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",val);} class]);
}

運行結果:

Stack Block:__NSStackBlock__

以上四種情況之外,都推薦使用block的copy實例方法把block復制到堆上。比如:
block為函數參數的時候,就需要我們手動的copy一份到堆上了。這里除去GCD API、系統框架中本身帶usingBlock的方法,其他我們自定義的方法傳遞Block為參數的時候都需要手動copy一份到堆上。例4:

id getBlockArray()
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"blk0:%d", val);},
            ^{NSLog(@"blk1:%d", val);}, nil];
}
int main(int argc, char * argv[]) {
    id obj = getBlockArray();
    void (^blk)(void) = [obj objectAtIndex:1];
    blk();
    return 0;
}

運行,這段程序崩潰。
在NSArray類的initWithObjects方法上傳遞block參數不屬于上面系統自動復制的情況(不屬于使用Cocoa框架含有usingBlock的方法傳遞block參數)。通過之前的分析,顯而易見^{NSLog(@"blk0:%d", val);}是StackBlock,在getBlockArray函數執行結束時,棧上的block被廢棄,因此在執行源代碼的[obj objectAtIndex:1]時,就發生異常。
解決辦法:手動復制

id getBlockArray()
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"blk0:%d", val) ;} copy],
            [^{NSLog(@"blk1:%d", val);} copy], nil];
}

int main(int argc, char * argv[]) {
    id obj = getBlockArray();
    void (^blk)(void) = [obj objectAtIndex:1];
    blk();
    return 0;
}

最后。ARC會自動處理block的內存,不用手動release,但MRC下需要,否則會內存泄漏。

3.3block的copy和release

copy

block的復制可以使用,Block_copy()函數又或者copy實例方法。
Block_copy()的實現。在Block.h文件中看到(在Xcode中也可以找到):

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))

Block_copy()的原型是_Block_copy()函數,而實際上最后調用的是_Block_copy_internal()函數:

//這里傳入的參數實際上就是Block
void *_Block_copy(const void *arg) {
    return _Block_copy_internal(arg, WANTS_ONE);
}

static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;

    //1.如果傳遞的參數為NULL,返回NULL。
    if (!arg) return NULL;
    
    //2.參數類型轉換。轉為指向Block_layout結構體的指針。Block_layout結構體請回顧文章開頭,相當于clang轉換后的__main_block_impl_0結構體,包括指向block的實現功能的指針和各種數據。
    aBlock = (struct Block_layout *)arg;

    //3.如果block的flags包含BLOCK_NEEDS_FREE,表明它是堆上的Block(為什么?見第7步注釋)
    //增加引用計數,返回相同的block
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    //這里刪掉了與垃圾回收(GC)相關的代碼,GC不做討論

    //4.如果是全局block,什么也不做,返回相同的block
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }

    // Its a stack block.  Make a copy.
    if (!isGC) {
        //5.能夠走到這里,表明是一個棧Block。需要復制到堆上。第一步申請內存
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
        //6.將棧數據復制到堆上
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        //7.更新block的flags
        //第一句后面的注釋說它不是必須的。
        result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
        //設置flags為BLOCK_NEEDS_FREE,表明它是一個堆block。內存支持它一旦引用計數=0,
        //就進行釋放。 “|1”是用來把block的引用計數設置為1。
        result->flags |= BLOCK_NEEDS_FREE | 1;
        //8.block的isa指針設置為_NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        //9.如果block有copy helper函數就調用它(和block所持有對象的內存管理有關,文章后面會講到這部分)
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
            //printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock);
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
    else {
        //GC相關
    }
}

對_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock這三種block,調用copy方法的總結:


不管block配置在哪里,調用copy方法進行復制不會產生任何問題。根據實際情況需要決定是否調用copy,如果在所有情況下都進行復制是不可取的做法,這樣會浪費cpu資源。

release

同樣地,block的釋放可以使用Block_release()函數或者release方法。

#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))

Block_release()原型是_Block_release()函數:

void _Block_release(void *arg) {
    //1.參數類型轉換,轉換為一個指向Block_layout結構體的指針。
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;

    //2.取出flags中表示引用計數的部分,并且對它遞減。
    int32_t newCount;
    newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;
    //3.如果引用計數>0,表明仍然有對block的引用,block不需要釋放
    if (newCount > 0) return;

    if (aBlock->flags & BLOCK_IS_GC) {
        //GC相關
    }
    //4.flags包含BLOCK_NEEDS_FREE(堆block),且引用計數=0
    else if (aBlock->flags & BLOCK_NEEDS_FREE) {
        //如果有copy helper函數就調用,釋放block捕獲的一些對象,對應_Block_copy_internal中的第9步
        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
        //釋放block
        _Block_deallocator(aBlock);
    }
    //5.全局Block,什么也不做
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        ;
    }
    //6.發生了一些奇怪的事情導致堆棧block視圖被釋放,打印日志警告開發者
    else {
        printf("Block_release called upon a stack Block: %p, ignored\n", (void *)aBlock);
    }
}

_Block_copy_internal()第9步和_Block_release()第4步中,block所持有對象的內存管理相關內容之后再詳細說明。

4.__block說明符

回顧在第二節中截獲自動變量值的例子。block在實現時捕獲自動變量的瞬時值,而且不允許在block內修改;因為超出棧作用域就會被釋放的原因,也無法用指針傳遞的方式來實現在block內修改自動變量。

我們知道使用__block 修飾自動變量就可以在block內改變外部自動變量的值。那__block又是怎樣實現這個目的的呢?以下分為基本數據類型、對象類型的指針變量來說明。

4.1基本數據類型的變量

例5:

int main(int argc, const char * argv[]) {

    __block int c = 3;
    void (^blk)(void) = ^{
        c++;
        NSLog(@"1--- c = %d",c);
    };

    c++;
    NSLog(@"2--- c = %d",c);
    blk();
    NSLog(@"3--- c = %d",c);

    return 0;
}

運行結果:

2--- c = 4
1--- c = 5
3--- c = 5

clang:

// __block為變量c創建的結構體,其中成員c為c的值,forwarding為指向自己的指針
struct __Block_byref_c_0 {
  void *__isa;
__Block_byref_c_0 *__forwarding;
 int __flags;
 int __size;
 int c;
};

// block結構體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_c_0 *c; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_c_0 *_c, int flags=0) : c(_c->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// block的函數實現
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_c_0 *c = __cself->c; // bound by ref

        (c->__forwarding->c)++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_944c40_mi_0,(c->__forwarding->c));
    }

//捕獲的變量的copy和release
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->c, (void*)src->c, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->c, 8/*BLOCK_FIELD_IS_BYREF*/);}

//block的描述結構體
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[]) {

    __attribute__((__blocks__(byref))) __Block_byref_c_0 c = {(void*)0,(__Block_byref_c_0 *)&c, 0, sizeof(__Block_byref_c_0), 3};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_c_0 *)&c, 570425344));

    (c.__forwarding->c)++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_944c40_mi_1,(c.__forwarding->c));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_944c40_mi_2,(c.__forwarding->c));

    return 0;
}

注意到,加了__block修飾的int c變量變成了:__Block_byref_c_0結構體類型的變量

__attribute__((__blocks__(byref))) __Block_byref_c_0 c = {(void*)0,(__Block_byref_c_0 *)&c, 0, sizeof(__Block_byref_c_0), 3};

__main_block_impl_0結構體中c變量不再是int類型了,而是變成了一個指向__Block_byref_c_0結構體的指針。__Block_byref_c_0結構如下:

struct __Block_byref_c_0 {
  void *__isa;
__Block_byref_c_0 *__forwarding;
 int __flags;
 int __size;
 int c;
};

__Block_byref_c_0結構體的成員變量__forwarding初始化為指向自身的指針。而原本自動變量的值3,也成為了結構體中的成員變量。如下__block int c = 3;變成__Block_byref_c_0類型的變量:

__Block_byref_c_0 c = {
  (void*)0,
  (__Block_byref_c_0 *)&c, //指向自己
  0, 
  sizeof(__Block_byref_c_0), 
  3//c的值
};

自動變量c加了__block,在clang編譯后變成了一個結構體__Block_byref_c_0。正是如此,這個值才能被多個block共享、并且不受棧幀生命周期的限制。(把__block 變量當成是對象)

看到block的結構體初始化,__Block_byref_c_0類型的變量c以指針形式進行傳遞

void (*blk)(void) = ((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0,
 &__main_block_desc_0_DATA,
 (__Block_byref_c_0 *)&c,
 570425344)
);

block 捕獲__block變量,捕獲的是對應結構體的變量的地址。

再看一下block執行部分的代碼:

__Block_byref_c_0 *c = __cself->c; // bound by ref
(c->__forwarding->c)++;

__Block_byref_c_0 *c = __cself->c;取到指向__Block_byref_c_0結構體類型的變量c的指針。
(c->__forwarding->c)++;然后通過__forwarding訪問到成員變量c,也就是原先的自動變量。

那么現在問題來了:
1.block作為回調執行時,局部變量已經出棧了,為什么這時代碼還能正常工作?
2.__forwarding初始化為指向自身的指針,為什么要通過它來取得我們要修改的變量而不是c->c直接取出呢?

__block變量的內存管理 - copy和release

//dst:目標地址 src:源地址
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->c, (void*)src->c, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->c, 8/*BLOCK_FIELD_IS_BYREF*/);}

在上面clang轉換的代碼中看到這樣兩個函數,簡單來說他們就是用來做__block的復制和釋放的,其后中調用到的_Block_object_assign()函數和_Block_object_dispose()函數源碼可以在runtime.c看到。BLOCK_FIELD_IS_BYREF是block截獲__block變量的特殊標志。
另外我們也留意到__main_block_desc_0結構體中多了兩個成員變量:

void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);

上面兩個函數以指針形式被賦值到__main_block_desc_0結構體成員變量copy和dispose中。

雖然這兩個函數沒有看到明顯的調用,但在block從棧復制到堆上時以及堆上的Block被廢棄時會調用到這些函數去處理__block變量(從第3.3節,block的copy函數源碼第9步和release函數第4步可知)。

以_Block_object_assign()函數為例,從上面的源碼截圖中可以得知,實際上它最后調用的是_Block_byref_assign_copy()函數。總結一下上面截圖函數所做的事情:

棧block通過copy復制到了了堆上。此時,block使用到的__block變量也會被復制到堆上并被block持有。如果block已經在堆上,再復制block也不會對所使用的__block有影響。


如果是多個block使用了同一個__block變量,那么,有多少個block被復制到堆上,堆上的__block變量就被多少個block持有。當__block變量沒有被任何block持有時(block被廢棄了),它就會被釋放。(__block的思考方式和oc的引用計數式內存管理是相似的,而且__block對應的結構體里也有__isa指針,所以在我看來也可以把__block變量當成對象來思考)

棧上__block變量被復制到堆上后,會將成員變量__forwarding指針從指向自己換成指向堆上的__block,而堆上__block的__forwarding才是指向自己。


這樣,不管__block變量是在棧上還是在堆上,都可以通過__forwarding來訪問到變量值。
因此例5代碼中,block內的^{c++;};和block外的c++;在clang中轉換為如下形式:(c->__forwarding->c)++;
到此,兩個問題都回答了。

總結:
1.block捕獲__block變量,捕獲的是對應結構體的變量的地址。
2.可以把__block當做對象來看待。當block復制到堆上,block使用到的__block變量也會被復制到堆上并被block持有。
至于release的過程,就相當于copy的逆過程,很好理解就不多說了。

block持有對象

另外,回顧第二節中的例2,block中使用到(默認)附有__strong修飾符的NSMutableString類對象的自動變量NSMutableString *str = [[NSMutableString alloc]initWithString:@"hello"];。轉換源碼之后,同樣地多了__main_block_copy_0__main_block_dispose_0函數。

因為在C語言的結構體中,編譯器沒法很好的進行初始化和銷毀操作。這樣對內存管理來說是很不方便的。所以就在 __main_block_desc_0結構體中間增加成員變量 void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*)和void (*dispose)(struct __main_block_impl_0*),利用OC的Runtime進行內存管理。

與__block相似,對象類型的指針變量被block截獲值(地址),而block被復制到堆上后持有這個對象,因此,它可以超出作用域而存在。當堆上block被廢棄時,釋放block持有的對象(不是持有變量)。指針指向的對象并不會隨block的復制而復制到堆上。


_Block_object_assign函數的調用相當于把對象retain了,因此block持有對象。

4.2對象類型的指針變量

__block NSObject *obj = [[NSObject alloc]init];
    NSLog(@"----%@,%p",obj,&obj);
    void (^blk)(void) = ^{
        NSLog(@"----%@,%p",obj,&obj);
    };
    blk();

clang:

//與__block普通類型變量相比,這個結構體體多了兩個成員變量
struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);//多出來的
 void (*__Block_byref_id_object_dispose)(void*);//多出來的
 NSObject *obj;
};
//這也多出來的,對應上面的copy函數指針
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
//多出來,對飲跟上面的dispose函數指針
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
//余下的部分基本和__block普通類型變量差不多
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_obj_0 *obj; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_obj_0 *_obj, int flags=0) : obj(_obj->__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_obj_0 *obj = __cself->obj; // bound by ref

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_a45e66_mi_1,(obj->__forwarding->obj),&(obj->__forwarding->obj));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 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[]) {
    __attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {(void*)0,(__Block_byref_obj_0 *)&obj, 33554432, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_a45e66_mi_0,(obj.__forwarding->obj),&(obj.__forwarding->obj));
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_obj_0 *)&obj, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}
__block NSObject *obj = [[NSObject alloc]init];
相當于
__block __strong NSObject *obj = [[NSObject alloc]init];

可以看到,和4.1節一樣,block 捕獲__block變量,捕獲的是對應結構體的變量的地址。并且當block從棧復制到堆上,__block變量從棧復制到堆,且堆__block變量持有賦值給它的對象。當__block變量被廢棄時,釋放賦值給__block變量的對象。

持有關系:堆Block -> 堆__block變量 -> 對象
只要堆上的__block變量存在,對象就繼續處于被持有的狀態。

總結一下以上4個章節:

  • 捕獲持有是兩個概念,不要混淆。(持有是MRC下的說法,而在ARC下的內存管理我們談的是“強弱指針引用”。)
  • block相當于是對象。
  • 能夠被block捕獲的變量:自動變量、靜態變量、__block變量。block捕獲:自動變量的值(基本數據類型-值,對象類型指針-對象地址);靜態變量的地址;__block變量則是其對應結構體變量的指針:地址。
  • 自動變量是值傳遞,所以不能在block內改變值。
  • __block變量和靜態變量是地址傳遞,可以在block內直接改變值。
  • 全局變量、靜態全局變量,因為作用域范圍廣,所以可以在block內改變它們的值
  • 為了解決block所在變量域結束后block仍然可用的問題,需要把棧block復制到堆上
  • ARC時,在四種情況下stackBlock會自動復制到堆上,其余時候必須手動copy才會復制到堆上;而MRC則不會,只有手動copy才會復制到堆上
  • __block變量也可以當成是對象看待。block復制到堆上時,它使用到的__block變量也會復制到堆上,無論MRC還是ARC。
  • block復制到堆上引起的持有對象的關系:“->”代表“持有”
對象類型變量:堆Block -> 對象
__block 普通基本數據類型變量:堆Block -> 堆__block變量
__block __strong 對象類型變量: 堆Block -> 堆__block變量 -> 對象
對象本身就在堆區,不存在復制不復制的說法,只是它被“持有”的數量有所增加
  • 在ARC下,__block會導致對象被retain。而在MRC下不會。

5.循環引用

循環引用是什么其實很多人應該都知道,這里簡單提一下。比如說:
1.多個對象之間相互引用形成環。A對象強引用B,B強引用A,于是兩者內存一直無法釋放。
2.對象自己引用自己。

例6:

#import <Foundation/Foundation.h>
typedef void (^PersonBlock)(void);
@interface Person : NSObject
@property (nonatomic ,assign) NSInteger age;
@property (nonatomic ,strong) NSString *name;
- (void)configurePersonBlock:(PersonBlock)blk_t;
@end

#import "Person.h"
@interface Person()
//不作為公有屬性,而是在對外方法接口中把Block傳進來
@property (nonatomic ,strong) PersonBlock blk;
@end

@implementation Person
- (void)configurePersonBlock:(PersonBlock)blk_t{
    self.blk = blk_t;
}

- (void)actionComplete{
    self.blk();
}
@end
#import "ViewController.h"
#import "BViewController.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(50, 50, 50, 50)];
    btn.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)click:(id)sender {
    BViewController *bVC = [[BViewController alloc]init];
    [self.navigationController pushViewController:bVC animated:YES];
}
@end
--------------------------------------------------------------------
#import "BViewController.h"
#import "Person.h"
@interface BViewController ()
@property (nonatomic ,strong) Person *person;
@property (nonatomic ,copy) NSString *str;
@end

@implementation BViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    [self.person configurePersonBlock:^{
        NSLog(@"printf str:%@",self.str);
    }];
    [self.person actionComplete];
}
@end
1.多個對象之間相互引用形成環。

成環:B控制器通過strong實例變量持有person對象,person持有block,block又持有self(即B控制器)。

block用到的外部的對象,mallocBlock會在內部持有它。
Block捕獲了實例變量_var,那么也會自動把self變量一起捕獲了,因為實例變量是與self所指代的實例相關聯在一起的。但是像例6這樣寫:[self.person configurePersonBlock:^{ NSLog(@"%ld",_var); }];由于沒有明確使用self變量,所以很容易就會忘記self也被捕獲了。而直接訪問實例變量和通過self來訪問是等效的,所以通常屬性來訪問實例變量,這樣就明確地使用了self了。
self也是對象,所以block捕獲它的時候也會持有該對象。

例7:

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
@property (nonatomic ,strong) Person *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person1 = [[Person alloc]init];
    person1.name = @"commet";
    person1.age = 18;
    [person1 configurePersonBlock:^{
        NSLog(@"%@",person1.name);
    }];
}
@end
2.自己引用自己

5.1解除循環引用

以例6為例分析:



例6的引用環是這樣的,只要打破其中一道引用,就能解除循環引用。

  • 解除①引用
    可以這么修改:
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    [self.person configurePersonBlock:^{
        NSLog(@"printf str:%@",self.str);
        self.person = nil;//改了這里
    }];
    [self.person actionComplete];
}
B控制器push沒有發生內存泄漏

ps:必須執行block才能解除①的引用。

  • 解除②引用
    在Person類中:
@implementation Person

- (void)configurePersonBlock:(PersonBlock)blk_t{
    self.blk = blk_t;
}

- (void)actionComplete{
    self.blk();
    self.blk = nil;//改了這句
}

然后在控制器中調用它:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    [self.person configurePersonBlock:^{
        NSLog(@"printf str:%@",self.str);
    }];
    [self.person actionComplete];
}
B控制器push還是沒有發生內存泄漏

但是前面這兩種做法又并不是那么合理,因為他們都強迫調用actionComplete這個方法來解除其中一層引用,但有時候你無法假定調用者一定會這么做。

  • 解除③引用
    block要使用的外部變量,作為block形參傳遞進block。
Person類
#import <Foundation/Foundation.h>
typedef void (^PersonBlock)(NSString *);

@interface Person : NSObject
@property (nonatomic ,assign) NSInteger age;
@property (nonatomic ,strong) NSString *name;

- (void)configurePersonBlock:(PersonBlock)blk_t;

- (void)actionComplete:(NSString *)str;
@end

#import "Person.h"
@interface Person()
@property (nonatomic ,strong) PersonBlock blk;
@end

@implementation Person

- (void)configurePersonBlock:(PersonBlock)blk_t{
    self.blk = blk_t;
}

- (void)actionComplete:(NSString *)str{
    self.blk(str);
}
@end
----------------------------------------------------------------

#import "BViewController.h"
#import "Person.h"
@interface BViewController ()
@property (nonatomic ,strong) Person *person;
@property (nonatomic ,copy) NSString *str;
@end

@implementation BViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    [self.person configurePersonBlock:^(NSString *str) {
        NSLog(@"printf str:%@",str);
    }];
    [self.person actionComplete:self.str];

}
@end
B控制器push依舊沒有發生內存泄漏

這種方法存在一個缺點,就是如果在block中要使用到很多外部變量、對象,那么就要給Block添加很多參數。

往往我們使用__weak來打破這種強引用。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    
    __weak typeof(self) weakself = self;
    [self.person configurePersonBlock:^ {
        NSLog(@"printf str:%@",weakself.str);
    }];
    [self.person actionComplete];

}

但也不是說在block中就一定要使用weakself,因為有時候循環引用未必存在:
比如說Masonry,一般我們是這樣寫的:

[_view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(60, 60));
        make.right.equalTo(self.view.mas_right).offset(-24);
        make.bottom.equalTo(self.view.mas_bottom).offset(-50);
    }];

顯然block引用了self,但這樣寫并沒有引起循環引用:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

在mas_makeConstraints這個方法中,可以看到self并沒有強引用block,而這個block只是作為參數傳遞進來并直接調用而已。

說完weakself那么不得不提起strongself了。Apple 官方文檔有講到,如果在 Block 執行完成之前,self 被釋放了,weakSelf 也會變為 nil。比如:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
        
    Person *person = [[Person alloc]init];
    person.name = @"commet";
    person.age = 18;
    
    __weak typeof(person) weakPerson = person;
    [person configurePersonBlock:^ {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"printf str:%@",weakPerson.name);
        });
    }];
    [person actionComplete];
}

運行結果:

printf str:(null)

[person actionComplete];調用block之后,viewDidLoad方法作用域結束后,person對象被釋放。由于dispatch_after的延遲執行,在Block執行完成前,捕獲的對象釋放了,block捕獲weakPerson變為nil。

由于weakself無法控制對象釋放時機所帶來的問題,我們在Block中使用__strong修飾weakself保證任何情況下self在超出作用域后仍能夠使用,防止self的提前釋放。

__weak typeof(person) weakPerson = person;
    [person configurePersonBlock:^ {
        __strong typeof(weakPerson) strongPerson = weakPerson;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"printf str:%@",strongPerson.name);
        });
    }];
    [person actionComplete];

當block執行完畢就會釋放自動變量strongSelf,釋放對self的強引用。
所以總結來說,weakself是用來解決block循環引用的問題的,而strongself是用來解決在block執行過程中self提前釋放的問題。

最后還有一種解除循環引用的方法:使用__block變量
修改一下例7:

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *person1 = [[Person alloc]init];
    person1.name = @"commet";
    person1.age = 18;
    
    __block Person *blkPerson = person1;
    
    [person1 configurePersonBlock:^{
        NSLog(@"%@",blkPerson.name);
        blkPerson = nil;
    }];
    person1.blk();
}


這段代碼沒有引起循環引用,但是如果沒有執行賦值給成員變量的blk的block(即刪掉person1.blk();這句),就會造成循環引用引起內存泄漏。person持有block,block持有__block變量,__block變量又持有person對象,于是就形成了保留環...

雖然使用__block可以控制對象的持有時間,在執行block時可以動態地決定是否將nil或者其他對象賦值在__block變量中,但它有一個缺點就是,必須執行一次block才能打破循環引用。

ps:在ARC下__block會導致對象被retain,有可能導致循環引用。而在MRC下,則不會retain這個對象,也不會導致循環引用。

參考文檔:
Block_private.h
runtime.c
文章:
A look inside blocks: Episode 3 (Block_copy)
objc 中的 block
談Objective-C block的實現
Block 小測驗
深入研究Block用weakSelf、strongSelf、@weakify、@strongify解決循環引用

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容