深入理解iOS的block(上)

前言

在文章之前,先抛出如下问题。

  • block的原理是怎样的?本质是什么?
  • __block的作用是什么?有什么使用注意点?
  • block的属性修饰词为什么是copy?使用block有哪些使用注意?
  • block一旦没有进行copy操作,就不会在堆上
  • block在修改NSMutableArray,需不需要添加__block?

导读

本文主要从如下几个方面讲解block

  • 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.

翻译过来表达就是

闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。

  • block 实际上就是 Objective-C 语言对于闭包的实现。

block的基本使用

  • block本质上也是一个OC对象,它内部也有个isa指针

  • block是封装了函数调用以及函数调用环境的OC对象

  • 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;

 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);

你可能感兴趣的:(深入理解iOS的block(上))