深刻理解iOS的block

本文首發於我的博客html

前言

在文章以前,先拋出以下問題。ios

  • block的原理是怎樣的?本質是什麼?
  • __block的做用是什麼?有什麼使用注意點?
  • block的屬性修飾詞爲何是copy?使用block有哪些使用注意?
  • block一旦沒有進行copy操做,就不會在堆上
  • block在修改NSMutableArray,需不須要添加__block?

若是如今不是很熟悉,但願看完這篇文章,能有個新的認識。git

導讀

本文主要從以下幾個方面講解blockgithub

  • block的基本使用
  • block在內存中的佈局
  • block對變量的捕獲分析
  • MRC和ARC的對比
  • __block的分析
  • block中內存管理問題
  • block致使的循環引用問題

什麼是block

先介紹一下什麼是閉包。在 wikipedia 上,閉包的定義是安全

In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.bash

翻譯過來表達就是數據結構

閉包是一個函數(或指向函數的指針),再加上該函數執行的外部的上下文變量(有時候也稱做自由變量)。閉包

  • block 實際上就是 Objective-C 語言對於閉包的實現。

block的基本使用

  • block本質上也是一個OC對象,它內部也有個isa指針iphone

  • block是封裝了函數調用以及函數調用環境的OC對象ide

  • block的底層結構以下圖

block的底層結構

無參無返回值的定義和使用

//無參無返回值 定義 和使用
void (^MyBlockOne)(void) = ^{
      NSLog(@"無參無返回值");
};
    
// 調用
MyBlockOne();

複製代碼

無參有返回值的定義和使用

// 無參有返回值
int (^MyBlockTwo)(void) = ^{
    NSLog(@"無參有返回值");
    return 2;
};
// 調用
int res = MyBlockTwo();
複製代碼

有參無返回值的定義和使用

//有參無返回值 定義
void (^MyBlockThree)(int a) = ^(int a){
    NSLog(@"有參無返回值 a = %d",a);
};
    
// 調用
MyBlockThree(10);
複製代碼

有參有返回值的定義和使用

//有參有返回值
int (^MyBlockFour)(int a) = ^(int a){
    NSLog(@"有參有返回值 a = %d",a);
    return a * 2;
};
MyBlockFour(4);
複製代碼

typedef 定義Block

實際開發中,常常須要把block做爲一個屬性,咱們能夠定義一個block

eg:定義一個有參有返回值的block

typedef int (^MyBlock)(int a, int b);
複製代碼

定義屬性的時候,以下便可持有這個block

@property (nonatomic,copy) MyBlock myBlockOne;
複製代碼

block實現

self.myBlockOne = ^int(int a, int b) {
      return a + b;
};
複製代碼

調用

self.myBlockOne(2, 5);
複製代碼

block 類型和數據結構

block 數據結構分析

生成cpp文件

以下代碼

int age = 20;
void (^block)(void) =  ^{
     NSLog(@"age is %d",age);
 };
        
block();

複製代碼
  • 打開終端,cd到當前目錄下

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

生成main.cpp

block 結構分析

int age = 20;

// block的定義
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// block的調用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

複製代碼

上面的代碼刪除掉一些強制轉換的代碼就就剩下以下所示

int age = 20;
void (*block)(void) = &__main_block_impl_0(
						__main_block_func_0, 
						&__main_block_desc_0_DATA, 
						age
						);
// block的調用
block->FuncPtr(block);
複製代碼

看出block的本質就是一個結構體對象,結構體__main_block_impl_0代碼以下

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
    //構造函數(相似於OC中的init方法) _age是外面傳入的
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    //isa指向_NSConcreteStackBlock 說明這個block就是_NSConcreteStackBlock類型的
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

結構體中第一個是struct __block_impl impl;

struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
};       
複製代碼

結構體中第二個是__main_block_desc_0;

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size; // 結構體__main_block_impl_0 佔用的內存大小
}
複製代碼

結構體中第三個是age

也就是捕獲的局部變量 age

__main_block_func_0

//封裝了block執行邏輯的函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_7f3f1b_mi_0,age);
}
複製代碼

用一幅圖來表示

變量捕獲

其實上面的代碼咱們已經看得出來變量捕獲了,這裏繼續詳細分析一下

變量類型 捕獲到block內部 訪問方式
局部變量 auto 值傳遞
局部變量 static 指針傳遞
全局變量 × 直接訪問

局部變量auto(自動變量)

  • 咱們平時寫的局部變量,默認就有 auto(自動變量,離開做用域就銷燬)
運行代碼

例以下面的代碼

int age = 20;
void (^block)(void) =  ^{
     NSLog(@"age is %d",age);
};
age = 25;
       
block();
複製代碼

等同於

auto int age = 20;
void (^block)(void) =  ^{
     NSLog(@"age is %d",age);
};
age = 25;
       
block();
複製代碼

輸出

20

分析

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

生成main.cpp

如圖所示

int age = 20;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 25;

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

struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;

NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_d36452_mi_5);
複製代碼

能夠知道,直接把age的值 20傳到告終構體__main_block_impl_0中,後面再修改age = 25並不能改變block裏面的值

局部變量 static

static修飾的局部變量,不會被銷燬

運行代碼

eg

static int height  = 30;
int age = 20;
void (^block)(void) =  ^{
     NSLog(@"age is %d height = %d",age,height);
};
age = 25;
height = 35;
block();
        
複製代碼

執行結果爲

age is 20 height = 35

複製代碼

能夠看得出來,block外部修改height的值,依然能影響block內部的值

分析

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

生成main.cpp

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *height = __cself->height; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_3146e1_mi_4,age,(*height));
        }

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[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 



        static int height = 30;
        int age = 20;
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
        age = 25;
        height = 35;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
複製代碼

如圖所示,age是直接值傳遞,height傳遞的是*height 也就是說直接把內存地址傳進去進行修改了。

全局變量

運行代碼
int age1 = 11;
static int height1 = 22;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) =  ^{
            NSLog(@"age1 is %d height1 = %d",age1,height1);
        };
        age1 = 25;
        height1 = 35;
        block();

    }
    return 0;
}
複製代碼

輸出結果爲

age1 is 25 height1 = 35


複製代碼
分析

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

生成main.cpp

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;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_4e8c40_mi_4,age1,height1);
}

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[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        age1 = 25;
        height1 = 35;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        
    }
    return 0;
}
複製代碼

從cpp文件能夠看出來,並無捕獲全局變量age1和height1,訪問的時候,是直接去訪問的,根本不須要捕獲

小結

變量類型 捕獲到block內部 訪問方式
局部變量 auto 值傳遞
局部變量 static 指針傳遞
全局變量 × 直接訪問
  • auto修飾的局部變量,是值傳遞
  • static修飾的局部變量,是指針傳遞

其實也很好理解,由於auto修飾的局部變量,離開做用域就銷燬了。那若是是指針傳遞的話,可能致使訪問的時候,該變量已經銷燬了。程序就會出問題。而全局變量原本就是在哪裏均可以訪問的,因此無需捕獲。

block類型

block也是一個OC對象

在進行分析block類型以前,先明確一個概念,那就是block中有isa指針的,block是一個OC對象,例以下面的代碼

void (^block)(void) =  ^{
      NSLog(@"123");
};

NSLog(@"block.class = %@",[block class]);
NSLog(@"block.class.superclass = %@",[[block class] superclass]);
NSLog(@"block.class.superclass.superclass = %@",[[[block class] superclass] superclass]);
NSLog(@"block.class.superclass.superclass.superclass = %@",[[[[block class] superclass] superclass] superclass]);
複製代碼

輸出結果爲

iOS-block[18429:234959] block.class = __NSGlobalBlock__
iOS-block[18429:234959] block.class.superclass = __NSGlobalBlock
iOS-block[18429:234959] block.class.superclass.superclass = NSBlock
iOS-block[18429:234959] block.class.superclass.superclass.superclass = NSObject

複製代碼

說明了上面代碼中的block的類型是__NSGlobalBlock,繼承關係能夠表示爲__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject

block有3種類型

block有3種類型,能夠經過調用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型

  • __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
  • __NSStackBlock__ ( _NSConcreteStackBlock )
  • __NSMallocBlock__ ( _NSConcreteMallocBlock )

其中三種不一樣的類型和環境對應以下

block類型 環境
__NSGlobalBlock__ 沒有訪問auto變量
__NSStackBlock__ 訪問了auto變量
__NSMallocBlock__ __NSStackBlock__調用了copy

其在內存中的分配以下對應

運行代碼查看

MRC下

注意,如下代碼在MRC下測試

注意,如下代碼在MRC下測試

注意,如下代碼在MRC下測試

由於ARC的時候,編譯器作了不少的優化,每每看不到本質,

  • 改成MRC方法: Build Settings 裏面的Automatic Reference Counting改成NO

以下圖所示

用代碼來表示

void (^block)(void) =  ^{
       NSLog(@"123");
};

NSLog(@"沒有訪問auto block.class = %@",[block class]);
        
        
auto int a = 10;
void (^block1)(void) =  ^{
      NSLog(@"a = %d",a);
};
        
NSLog(@"訪問了auto block1.class = %@",[block1 class]);
               
NSLog(@"訪問量auto 而且copy block1-copy.class = %@",[[block1 class] copy]);
複製代碼

輸出爲

OS-block[23542:349513] 沒有訪問auto block.class = __NSGlobalBlock__
iOS-block[23542:349513] 訪問了auto block1.class = __NSStackBlock__
iOS-block[23542:349513] 訪問量auto 而且copy block1-copy.class = __NSStackBlock__
複製代碼

能夠看出和上面說的

block類型 環境
__NSGlobalBlock__ 沒有訪問auto變量
__NSStackBlock__ 訪問了auto變量
__NSMallocBlock__ __NSStackBlock__調用了copy

是一致的

ARC下

在ARC下,上面的代碼輸出結果爲下面所示,由於編譯器作了copy

iOS-block[24197:358752] 沒有訪問auto block.class = __NSGlobalBlock__
iOS-block[24197:358752] 訪問了auto block1.class = __NSMallocBlock__
iOS-block[24197:358752] 訪問量auto 而且copy block1-copy.class = __NSMallocBlock__
複製代碼

block的copy

前面說了在ARC環境下,編譯器會根據狀況自動將棧上的block複製到堆上,具體來講好比如下狀況

copy的狀況

  • block做爲函數返回值時
  • 將block賦值給__strong指針時
  • block做爲Cocoa API中方法名含有usingBlock的方法參數時
  • block做爲GCD API的方法參數時
block做爲函數返回值時

// 定義Block
typedef void (^YZBlock)(void);

// 返回值爲Block的函數
YZBlock myblock()
{
    int a = 6;
    return ^{
        NSLog(@"--------- %d",a);
    };
}

YZBlock Block = myblock();
Block();
NSLog(@" [Block class] = %@", [Block class]);
複製代碼

輸出爲

iOS-block[25857:385868] --------- 6
iOS-block[25857:385868]  [Block class] = __NSMallocBlock__
複製代碼

上述代碼若是再MRC下輸出__NSStackBlock__,在ARC下,自動copy,因此是__NSMallocBlock__

將block賦值給__strong指針時
// 定義Block
typedef void (^YZBlock)(void);

int b = 20;
YZBlock Block2 = ^{
    NSLog(@"abc %d",b);
};
NSLog(@" [Block2 class] = %@", [Block2 class]);

複製代碼

輸出爲

iOS-block[26072:389164]  [Block2 class] = __NSMallocBlock__
複製代碼

上述代碼若是再MRC下輸出__NSStackBlock__,在ARC下,自動copy,因此是__NSMallocBlock__

block做爲Cocoa API中方法名含有usingBlock的方法參數時

eg:

NSArray *array = @[@1,@4,@5];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // code
}];
複製代碼
block做爲GCD API的方法參數時

eg

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
            
});    
       
        
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //code to be executed after a specified delay
});
複製代碼

MRC下block屬性的建議寫法

  • @property (copy, nonatomic) void (^block)(void);

ARC下block屬性的建議寫法

  • @property (strong, nonatomic) void (^block)(void);
  • @property (copy, nonatomic) void (^block)(void);

對象類型的auto變量

例子一

首先看一個簡單的例子 定義一個類 YZPerson,裏面只有一個dealloc方法

@interface YZPerson : NSObject
@property (nonatomic ,assign) int age;
@end


@implementation YZPerson

- (void)dealloc
{
    NSLog(@"%s",__func__);
}

@end
複製代碼

以下代碼使用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        {
            YZPerson *person = [[YZPerson alloc]init];
            person.age = 10;
        }
        NSLog(@"-----");
    }
    return 0;
}
複製代碼

想必你們都能知道會輸出什麼,沒錯,就是person先銷燬,而後打印----- 由於person是在大括號內,當大括號執行完以後,person 就銷燬了。

iOS-block[1376:15527] -[YZPerson dealloc]
iOS-block[1376:15527] -----
複製代碼

例子二

上面的例子,是否是挺簡單,那下面這個呢,

// 定義block
typedef void (^YZBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YZBlock block;
        
        {
            YZPerson *person = [[YZPerson alloc]init];
            person.age = 10;
            
            block = ^{
                NSLog(@"---------%d", person.age);
            };
            
             NSLog(@"block.class = %@",[block class]);
        }
        NSLog(@"block銷燬");

    }
    return 0;
}


複製代碼

以下結果,輸出可知當 block爲__NSMallocBlock__類型時候,block能夠保住person的命的,由於person離開大括號以後沒有銷燬,當block銷燬,person才銷燬

iOS-block[3186:35811] block.class = __NSMallocBlock__
iOS-block[3186:35811] block銷燬
iOS-block[3186:35811] -[YZPerson dealloc]
複製代碼

分析

終端執行這行指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.mmain.m生成main.cpp 能夠 看到以下代碼

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  YZPerson *person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *_person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

很明顯就是這個block裏面包含 YZPerson *person

MRC下 block引用實例對象

上面的例子,是否是挺簡單,那若是是MRC下呢

// 定義block
typedef void (^YZBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YZBlock block;
        
        {
            YZPerson *person = [[YZPerson alloc]init];
            person.age = 10;
            
            block = ^{
                NSLog(@"---------%d", person.age);
            };
            
            NSLog(@"block.class = %@",[block class]);

            // MRC下,須要手動釋放
            [person release];
        }
        NSLog(@"block銷燬");
        // MRC下,須要手動釋放
		 [block release];
    }
    return 0;
}
複製代碼

輸出結果爲

iOS-block[3114:34894] block.class = __NSStackBlock__
iOS-block[3114:34894] -[YZPerson dealloc]
iOS-block[3114:34894] block銷燬
複製代碼

和上面的對比,區別就是,尚未執行NSLog(@"block銷燬");的時候,[YZPerson dealloc]已經執行了。也就是說,person 離開大括號,就銷燬了。

輸出可知當 block爲__NSStackBlock__類型時候,block不能夠保住person的命的

MRC下 [block copy]引用實例對象

在MRC下,對block執行了copy操做

// 定義block
typedef void (^YZBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YZBlock block;
        
        {
            YZPerson *person = [[YZPerson alloc]init];
            person.age = 10;
            
            block = [^{
                NSLog(@"---------%d", person.age);
            } copy];
            
			NSLog(@"block.class = %@",[block class]);
            // MRC下,須要手動釋放
            [person release];
        }
       
        NSLog(@"block銷燬");
        [block release];
    }
    return 0;

複製代碼

輸出結果爲,可知當 block爲__NSMallocBlock__類型時候,block是能夠保住person的命的

iOS-block[3056:34126] block.class = __NSMallocBlock__
iOS-block[3056:34126] block銷燬
iOS-block[3056:34126] -[YZPerson dealloc]
複製代碼

__weak修飾

  • 以下代碼
// 定義block
typedef void (^YZBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YZBlock block;
        
        {
            YZPerson *person = [[YZPerson alloc]init];
            person.age = 10;

            __weak YZPerson *weakPerson = person;
            
            block = ^{
                NSLog(@"---------%d", weakPerson.age);
            };
            
             NSLog(@"block.class = %@",[block class]);
        }
       
        NSLog(@"block銷燬");
    }
    return 0;
}
複製代碼
  • 輸出爲
iOS-block[3687:42147] block.class = __NSMallocBlock__
iOS-block[3687:42147] -[YZPerson dealloc]
iOS-block[3687:42147] block銷燬
複製代碼
  • 生成cpp文件

注意:

  • 在使用clang轉換OC爲C++代碼時,可能會遇到如下問題 cannot create __weak reference in file using manual reference

  • 解決方案:支持ARC、指定運行時系統版本,好比 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

生成以後,能夠看到,以下代碼,MRC狀況下,生成的代碼明顯多了,這是由於ARC自動進行了copy操做

//copy 函數
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  
  //dispose函數
  void (*dispose)(struct __main_block_impl_0*);
複製代碼
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //weak修飾
  YZPerson *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  //copy 函數
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  
  //dispose函數
  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
};


//copy函數內部會調用_Block_object_assign函數
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {

//asssgin會對對象進行強引用或者弱引用
_Block_object_assign((void*)&dst->person, 
(void*)src->person, 
3/*BLOCK_FIELD_IS_OBJECT*/);
}

//dispose函數內部會調用_Block_object_dispose函數
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->person, 
3/*BLOCK_FIELD_IS_OBJECT*/);
}

複製代碼

小結

不管是MAC仍是ARC

  • 當block爲__NSStackBlock__類型時候,是在棧空間,不管對外面使用的是strong 仍是weak 都不會對外面的對象進行強引用
  • 當block爲__NSMallocBlock__類型時候,是在堆空間,block是內部的_Block_object_assign函數會根據strong或者 weak對外界的對象進行強引用或者弱引用。

其實也很好理解,由於block自己就在棧上,本身都隨時可能消失,怎麼能保住別人的命呢?

  • 當block內部訪問了對象類型的auto變量時

  • 若是block是在棧上,將不會對auto變量產生強引用

  • 若是block被拷貝到堆上

    • 會調用block內部的copy函數
    • copy函數內部會調用_Block_object_assign函數
    • _Block_object_assign函數會根據auto變量的修飾符(__strong、__weak、__unsafe_unretained)作出相應的操做,造成強引用(retain)或者弱引用
  • 若是block從堆上移除

    • 會調用block內部的dispose函數
    • dispose函數內部會調用_Block_object_dispose函數
    • _Block_object_dispose函數會自動釋放引用的auto變量(release)
函數 調用時機
copy函數 棧上的Block複製到堆上
dispose函數 堆上的block被廢棄時

__block

先從一個簡單的例子提及,請看下面的代碼

// 定義block
typedef void (^YZBlock)(void);

int age = 10;
YZBlock block = ^{
    NSLog(@"age = %d", age);
};
block();
複製代碼

代碼很簡單,運行以後,輸出

age = 10

上面的例子在block中訪問外部局部變量,那麼問題來了,若是想在block內修改外部局部的值,怎麼作呢?

修改局部變量的三種方法

寫成全局變量

咱們把a定義爲全局變量,那麼在哪裏均可以訪問,

// 定義block
typedef void (^YZBlock)(void);
 int age = 10;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        YZBlock block = ^{
            age = 20;
            NSLog(@"block內部修改以後age = %d", age);
        };
        
        block();
        NSLog(@"block調用完 age = %d", age);
    }
    return 0;
}
複製代碼

這個很簡單,輸出結果爲

block內部修改以後age = 20
block調用完 age = 20
複製代碼

對於輸出就結果也沒什麼問題,由於全局變量,是全部地方均可訪問的,在block內部能夠直接操做age的內存地址的。調用完block以後,全局變量age指向的地址的值已經被更改成20,因此是上面的打印結果

static修改局部變量

// 定義block
typedef void (^YZBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       static int age = 10;
        YZBlock block = ^{
            age = 20;
            NSLog(@"block內部修改以後age = %d", age);
        };
        
        block();
        NSLog(@"block調用完 age = %d", age);
    }
    return 0;
}
複製代碼

上面的代碼輸出結果爲

block內部修改以後age = 20
block調用完 age = 20
複製代碼

終端執行這行指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.mmain.m生成main.cpp 能夠 看到以下代碼

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *age = __cself->age; // bound by copy
    
    (*age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_5dbaa1_mi_0, (*age));
}

複製代碼

能夠看出,當局部變量用static修飾以後,這個block內部會有個成員是int *age,也就是說把age的地址捕獲了。這樣的話,固然在block內部能夠修改局部變量age了。

  • 以上兩種方法,雖然能夠達到在block內部修改局部變量的目的,可是,這樣作,會致使內存沒法釋放。不管是全局變量,仍是用static修飾,都沒法及時銷燬,會一直存在內存中。不少時候,咱們只是須要臨時用一下,當不用的時候,能銷燬掉,那麼第三種,也就是今天的主角 __block隆重登場

__block來修飾

代碼以下

// 定義block
typedef void (^YZBlock)(void);


int main(int argc, const char * argv[]) {
    @autoreleasepool {
       __block int age = 10;
        YZBlock block = ^{
            age = 20;
            NSLog(@"block內部修改以後age = %d",age);
        };
        
        block();
        NSLog(@"block調用完 age = %d",age);
    }
    return 0;
}
複製代碼

輸出結果和上面兩種同樣

block內部修改以後age = 20
block調用完 age = 20
複製代碼

__block分析

  • 終端執行這行指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.mmain.m生成main.cpp

首先能發現 多了__Block_byref_age_0結構體

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    // 這裏多了__Block_byref_age_0類型的結構體
  __Block_byref_age_0 *age; // by ref
    // fp是函數地址  desc是描述信息  __Block_byref_age_0 類型的結構體  *_age  flags標記
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp; //fp是函數地址
    Desc = desc;
  }
};


複製代碼

再仔細看結構體__Block_byref_age_0,能夠發現第一個成員變量是isa指針,第二個是指向自身的指針__forwarding

// 結構體 __Block_byref_age_0
struct __Block_byref_age_0 {
    void *__isa; //isa指針
    __Block_byref_age_0 *__forwarding; // 指向自身的指針
    int __flags;
    int __size;
    int age; //使用值
};
複製代碼

查看main函數裏面的代碼

// 這是原始的代碼 __Block_byref_age_0
 __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {
 (void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        
             
// 這是原始的 block代碼
YZBlock block = ((void (*)())&__main_block_impl_0(
(void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));

複製代碼

代碼太長,簡化一下,去掉一些強轉的代碼,結果以下

// 這是原始的代碼 __Block_byref_age_0
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        
//這是簡化以後的代碼 __Block_byref_age_0
__Block_byref_age_0 age = {
     0, //賦值給 __isa
     (__Block_byref_age_0 *)&age,//賦值給 __forwarding,也就是自身的指針
      0, // 賦值給__flags
      sizeof(__Block_byref_age_0),//賦值給 __size
      10 // age 使用值
    };
        
// 這是原始的 block代碼
YZBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        
// 這是簡化以後的 block代碼
YZBlock block = (&__main_block_impl_0(
             		__main_block_func_0,
           		&__main_block_desc_0_DATA,
	           	 &age,
            	570425344));
        
 ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        //簡化爲
block->FuncPtr(block);

複製代碼

其中__Block_byref_age_0結構體中的第二個(__Block_byref_age_0 *)&age賦值給上面代碼結構體__Block_byref_age_0中的第二個__Block_byref_age_0 *__forwarding,因此__forwarding 裏面存放的是指向自身的指針

//這是簡化以後的代碼 __Block_byref_age_0
__Block_byref_age_0 age = {
     0, //賦值給 __isa
     (__Block_byref_age_0 *)&age,//賦值給 __forwarding,也就是自身的指針
      0, // 賦值給__flags
      sizeof(__Block_byref_age_0),//賦值給 __size
      10 // age 使用值
    };
複製代碼

結構體__Block_byref_age_0中代碼以下,第二個__forwarding存放指向自身的指針,第五個age裏面存放局部變量

// 結構體 __Block_byref_age_0
struct __Block_byref_age_0 {
    void *__isa; //isa指針
    __Block_byref_age_0 *__forwarding; // 指向自身的指針
    int __flags;
    int __size;
    int age; //使用值
};
複製代碼

調用的時候,先經過__forwarding找到指針,而後去取出age值。

(age->__forwarding->age));
複製代碼

小結

  • __block能夠用於解決block內部沒法修改auto變量值的問題

  • __block不能修飾全局變量、靜態變量(static)

    • 編譯器會將__block變量包裝成一個對象

調用的是,從__Block_byref_age_0的指針找到 age所在的內存,而後修改值

內存管理問題

bloc訪問OC對象

代碼以下

當block內部訪問外面的OC對象的時候

eg:

// 定義block
typedef void (^YZBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
     
        NSObject *obj = [[NSObject alloc]init];
        YZBlock block = ^{
            NSLog(@"%p",obj);
        };
         block();
    }
    return 0;
}

複製代碼

在終端使用clang轉換OC爲C++代碼

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
複製代碼

由於是在ARC下,因此會copy,棧上拷貝到堆上,結構體__main_block_desc_0中有copydispose

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*);
}
複製代碼

copy會調用 __main_block_copy_0

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, 3/*BLOCK_FIELD_IS_OBJECT*/);}
複製代碼

其內部的_Block_object_assign會根據代碼中的修飾符 strong或者weak而對其進行強引用或者弱引用。

查看__main_block_impl_0

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //strong 強引用
  NSObject *__strong obj;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

複製代碼

能夠看上修飾符是strong,因此,調用_Block_object_assign時候,會對其進行強引用。

由前面可知

  • 當block在棧上時,並不會對__block變量產生強引用

  • 當block被copy到堆時

    • 會調用block內部的copy函數
    • copy函數內部會調用_Block_object_assign函數
    • _Block_object_assign函數會對__block變量造成強引用(retain)
  • 當block從堆中移除時

    • 會調用block內部的dispose函數
    • dispose函數內部會調用_Block_object_dispose函數
    • _Block_object_dispose函數會自動釋放引用的__block變量(release)

拷貝

拷貝的時候,

  • 會調用block內部的copy函數
    • copy函數內部會調用_Block_object_assign函數
    • _Block_object_assign函數會對__block變量造成強引用(retain)

中咱們知道,以下代碼

__block int age = 10;
    YZBlock block = ^{
        age = 20;
        NSLog(@"block內部修改以後age = %d",age);
    };
複製代碼

局部變量age是在棧上的,在block內部引用age,可是當block從棧上拷貝到堆上的時候,怎麼能保證下次block訪問age的時候,能訪問到呢?由於咱們知道棧上的局部變量,隨時會銷燬的。

假設如今有兩個棧上的block,分別是block0和block1,同時引用了了棧上的__block變量。如今對block0進行copy操做,咱們知道,棧上的block進行copy,就會複製到堆上,也就是說block0會複製到堆上,由於block0持有__block變量,因此也會把這個__block變量複製到堆上,同時堆上的block0對堆上的__block變量是強引用,這樣能達到block0隨時能訪問__block變量

仍是上面的例子,剛纔block0拷貝到堆上了,如今若是block1也拷貝到堆上,由於剛纔變量已經拷貝到堆上,就不須要再次拷貝,只須要把堆上的block1也強引用堆上的變量就能夠了。

釋放

當釋放的時候

  • 會調用block內部的dispose函數
    • dispose函數內部會調用_Block_object_dispose函數
    • _Block_object_dispose函數會自動釋放引用的__block變量(release)

上面的代碼中,若是在堆上只有一個block引用__block變量,當block銷燬時候,直接銷燬堆上的__block變量,可是若是有兩個block引用__block變量,就須要當兩個block都廢棄的時候,纔會廢棄__block變量

其實,說到底,就是誰使用,誰負責

對象類型的auto變量__block變量

把前面的都放在一塊兒整理一下,有 auto 變量 num , __block變量int, obj 和weakObj2以下

__block int age = 10;
    int num = 8;
    NSObject *obj = [[NSObject alloc]init];
    NSObject *obj2 = [[NSObject alloc]init];
    __weak NSObject *weakObj2 = obj2;
    YZBlock block = ^{
        NSLog(@"age = %d",age);
        NSLog(@"num = %d",num);
        NSLog(@"obj = %p",obj);
        NSLog(@"weakObj2 = %p",weakObj2);
        NSLog(@"block內部修改以後age = %d",age);
	};
    
block();
複製代碼

執行終端指令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
複製代碼

生成代碼以下所示

被__block修飾的對象類型

  • __block變量在棧上時,不會對指向的對象產生強引用

  • __block變量被copy到堆時

    • 會調用__block變量內部的copy函數
    • copy函數內部會調用_Block_object_assign函數
    • _Block_object_assign函數會根據所指向對象的修飾符(__strong__weak__unsafe_unretained)作出相應的操做,造成強引用(retain)或者弱引用(注意:這裏僅限於ARC時會retain,MRC時不會retain)
  • 若是__block變量從堆上移除

    • 會調用__block變量內部的dispose函數
    • dispose函數內部會調用_Block_object_dispose函數
    • _Block_object_dispose函數會自動釋放指向的對象(release)

__block__forwarding指針

//結構體__Block_byref_obj_0中有__forwarding
 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 *__strong obj;
};

// 訪問的時候
age->__forwarding->age

複製代碼

爲啥什麼不直接用age,而是age->__forwarding->age呢?

這是由於,若是__block變量在棧上,就能夠直接訪問,可是若是已經拷貝到了堆上,訪問的時候,還去訪問棧上的,就會出問題,因此,先根據__forwarding找到堆上的地址,而後再取值

總結

  • 當block在棧上時,對它們都不會產生強引用

  • 當block拷貝到堆上時,都會經過copy函數來處理它們

    • __block變量(假設變量名叫作a)
  • _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

  • 對象類型的auto變量(假設變量名叫作p) _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

  • 當block從堆上移除時,都會經過dispose函數來釋放它們 __block變量(假設變量名叫作a) _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

  • 對象類型的auto變量(假設變量名叫作p) _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

循環引用問題

繼續探索一下block的循環引用問題。

看以下代碼,有個Person類,裏面兩個屬性,分別是block和age

#import <Foundation/Foundation.h>

typedef void (^YZBlock) (void);

@interface YZPerson : NSObject
@property (copy, nonatomic) YZBlock block;
@property (assign, nonatomic) int age;
@end


#import "YZPerson.h"

@implementation YZPerson
- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

複製代碼

main.m中以下代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        YZPerson *person = [[YZPerson alloc] init];
        person.age = 10;
        person.block = ^{
             NSLog(@"person.age--- %d",person.age);
        };
        NSLog(@"--------");

    }
    return 0;
}
複製代碼

輸出只有

iOS-block[38362:358749] --------

也就是說程序結束,person都沒有釋放,形成了內存泄漏。

循環引用緣由

下面這行代碼,是有個person指針,指向了YZPerson對象

YZPerson *person = [[YZPerson alloc] init];
複製代碼

執行完

person.block = ^{
             NSLog(@"person.age--- %d",person.age);
        };
複製代碼

以後,block內部有個強指針指向person,下面代碼生成cpp文件

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    //強指針指向person
  YZPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

而block是person的屬性

@property (copy, nonatomic) YZBlock block;
複製代碼

當程序退出的時候,局部變量person銷燬,可是因爲MJPerson和block直接,互相強引用,誰都釋放不了。

__weak解決循環引用

爲了解決上面的問題,只須要用__weak來修飾,便可

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YZPerson *person = [[YZPerson alloc] init];
        person.age = 10;
        
        __weak YZPerson *weakPerson = person;
        
        person.block = ^{
            NSLog(@"person.age--- %d",weakPerson.age);
        };
        NSLog(@"--------");

    }
    return 0;
}
複製代碼

編譯完成以後是

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    // block內部對weakPerson是弱引用
  YZPerson *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

當局部變量消失時候,對於YZPseson來講,只有一個若指針指向它,那它就銷燬,而後block也銷燬。

__unsafe_unretained解決循環引用

除了上面的__weak以後,也能夠用__unsafe_unretained來解決循環引用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YZPerson *person = [[YZPerson alloc] init];
        person.age = 10;
        
        __unsafe_unretained YZPerson *weakPerson = person;
        
        person.block = ^{
            NSLog(@"person.age--- %d",weakPerson.age);
        };
        NSLog(@"--------");

    }
    return 0;
}
複製代碼

對於的cpp文件爲

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  YZPerson *__unsafe_unretained weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *__unsafe_unretained _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

雖然__unsafe_unretained能夠解決循環引用,可是最好不要用,由於

  • __weak:不會產生強引用,指向的對象銷燬時,會自動讓指針置爲nil
  • __unsafe_unretained:不會產生強引用,不安全,指向的對象銷燬時,指針存儲的地址值不變

__block解決循環引用

eg:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       __block YZPerson *person = [[YZPerson alloc] init];
        person.age = 10;
        person.block = ^{
            NSLog(@"person.age--- %d",person.age);
            //這一句不能少
            person = nil;
        };
        // 必須調用一次
        person.block();
        NSLog(@"--------");
    }
    return 0;
}
複製代碼

上面的代碼中,也是能夠解決循環引用的。可是須要注意的是,person.block();必須調用一次,爲了執行person = nil;.

對應的結果以下

  • 下面的代碼,block會對__block產生強引用
__block YZPerson *person = [[YZPerson alloc] init];
person.block = ^{
        NSLog(@"person.age--- %d",person.age);
        //這一句不能少
        person = nil;
};
複製代碼
  • person對象自己就對block是強引用
@property (copy, nonatomic) YZBlock block;
複製代碼
  • __block對person產生強引用
struct __Block_byref_person_0 {
  void *__isa;
__Block_byref_person_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
    //`__block`對person產生強引用
 YZPerson *__strong person;
};
複製代碼

因此他們的引用關係如圖

當執行完person = nil時候,__block解除對person的引用,進而,全都解除釋放了。 可是必須調用person = nil才能夠,不然,不能解除循環引用

小結

經過前面的分析,咱們知道,ARC下,上面三種方式對比,最好的是__weak

MRC下注意點

若是再MRC下,由於不支持弱指針__weak,因此,只能是__unsafe_unretained或者__block來解決循環引用

結束

回到最開始的問題

  • block的原理是怎樣的?本質是什麼?

  • __block的做用是什麼?有什麼使用注意點?

  • block的屬性修飾詞爲何是copy?使用block有哪些使用注意?

  • block一旦沒有進行copy操做,就不會在堆上

  • block在修改NSMutableArray,需不須要添加__block?

如今是否是心中有了本身的答案呢?

參考資料:

唐巧談Objective-C block的實現

A look inside blocks: Episode 3 (Block_copy)

iOS底層原理

更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。

相關文章
相關標籤/搜索