iOS_理解Block(代码块)+底层实现

在这里插入图片描述

一、Block的基础知识

​ Block是对象,可以赋值,当参数,也可以放入Array和Dictionary中,可以retain和release。Block用“^”符号来表示,后面跟一对花括号,括号里是Block的实现代码。例如,下面就是一个简单的Block:

^{
  // Block implementation here
}

​ Block其实就是一个值,而且有其相关类型。与int、float或Objective-C对象一样,也可以把块复制给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:

void (^someBlock)() = ^{
  // Block implementation here
}

​ 这段代码定义了一个名为someBlock的变量。由于变量名写在正中间,所以看上去有点怪。然而Block会在不同的场景下有各种不同的写法。

​ Block难记的语法:

// 作为变量:
returnType (^blockName)(parameterTypes) = ^returnType(parameter) {};
// 作为属性:
@property (nonatomic, copy) returnType (^blockName)(parameterTypes);
// 作为函数声明中的参数:
- (void)someMethodBlock:(returnType (^)(parameterTypes))blockName;
// 作为函数调用中的参数:
[obj someMethodBlock:^returnType (parameters) {...}];
// 作为typedef:
typedef returnType (^TypeName)(parameterTypes); // 为block类型重命名
TypeName blockName = ^returnType(parameters) {}

// 例如:
// Block字面值:
double ^(double first, double second) {
    return first + second;
}
// 声明并使用:
double (^total)(double, double) = ^double (double first, double second) {
    return first + second;
};
double result = total(2, 4);

二、Block的类型

​ 块的强大之处:在声明它的范围里,所有变量都可以为其所捕获。也就是说,那个范围里的全部变量,在块里依然可以用。下面来举例说明一下各种类型的块:

1、全局块(Global Block)

​ 当Block没有访问auto变量(自动变量,离开了其作用域就会被销毁的),即:访问了全局变量 or 访问了静态局部变量 or 没有访问变量,此时的Block属于全局块NSConcreteGlobalBlock。块所使用的整个内存区域,在编译期已经完全确定了,因此全局块可以声明在全局内存里,而不需要在每次用到的时候于栈中创建(跟其他类型的block相比)。对全局块执行copy是一个空操作,因为全局块绝不可能为系统所回收。这种块实际上相当于单例。下面就是一个全局块:

// NSGlobalBlock:存储于全局数据区,由系统管理
// 没有访问auto变量(也叫自动变量,离开作用域就销毁的变量)
// 如:没有访问任何变量 或 访问了全局变量 或 访问了静态局部变量
static int localCount = 0; // 静态局部变量
void(^blockA)(void) = ^{
  NSLog(@"blockA"); // 没有访问任何变量
  NSLog(@"%@", [MOManager sharedInstance].name); // 访问了全局变量
  NSLog(@"%d", localCount); // 访问了静态局部变量
};
NSLog(@"blockA: %@", [blockA class]);

由于运行该块所需的全部信息在编译期确定,所以可把它做成全局块。这完全是一种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及销毁该块时执行一些无谓的操作。

2、栈块(Stack Block)

​ 当Block访问了栈区的变量(如:局部变量),此时的Block存储在栈区。其所占用的内存区域是分配在栈中的。也就是说,Block只在定义它的那个范围内有效。下面就是一个栈块:

// NSStackBlock 栈
// 访问了auto变量(即:栈区变量/局部变量), 并且 没有被`强指针`引用时!!!
int count = 0; // 局部变量
MOPerson *obj = [[MOPerson alloc] init]; // 局部变量
NSLog(@"blockB: %@", [^{
  NSLog(@"%d", count); // 访问了局部变量
  NSLog(@"%@", obj.name); // 访问了局部变量
  NSLog(@"%@", self.property); // 访问了局部变量
} class]);
3、堆块(Malloc Block)

​ 当Block访问了堆区的变量(如:alloc创建的对象)或 “栈块”调用了copy,此时的Block属于“堆块”。其所占用的内存区域是分配在堆中的。拷贝到堆上Block就成了带引用计数的对象了。后续再执行copy都不会真的执行复制,只是递增Block对象的引用计数。下面就是一个堆块:

// NSMallocBlock 堆
// 访问了auto变量,并且 被`强指针`引用时!!!
void(^blockC)(void) = ^{
  NSLog(@"%d", count); // 访问了局部变量
  NSLog(@"%@", obj.name); // 访问了局部变量
  NSLog(@"%@", self.property); // 访问了局部变量
};
NSLog(@"blockC: %@", [blockC class]);

​ 将Block的类型列表如下:

Type实现copy
Global(全局块)没有访问auto变量(离开作用域就销毁的变量),即:访问全局变量/静态局部变量/没有访问变量什么也不做
Stack(栈块)访问了auto变量(即:栈区变量/局部变量), 并且 没有被强指针引用时!!!复制到堆区
Malloc(堆块)stack Block 调用copy函数时;访问了auto变量,并且 被强指针引用时!!!;ARC模式下,以下4种情况被自动copy引用计数+1
4、ARC自动copy的块
  1. Block作为函数返回值
  2. Block赋值给__strong指针时(即被强指正引用时,如:__strong修饰的id类型/Block类型的成员变量)
  3. Block作为Cocoa API中的方法含有usingBlock的方法参数时
  4. Block作为GCD API的方法参数时

三、Block捕获变量+底层实现

先看一下概览:

static int localCount = 0; // 静态局部变量
int count = 0; // 局部变量
MOPerson *obj = [[MOPerson alloc] init];
NSMutableArray *arr = [NSMutableArray array];

__block int blockCount = count;
__block MOPerson *blockObj = obj;
__block typeof(arr) blockArr = arr;

void (^aBlock)(void) = ^{
  // 直接访问
  NSLog(@"%d", globalCount); // 访问全局变量:直接使用,结构不变
  NSLog(@"%d", localCount); // 访问静态局部变量:拷贝指针 到block的结构体 中使用
  NSLog(@"%d", count); // 访问局部基本数据类型:拷贝值 到block的结构体 中使用
  NSLog(@"%@", obj.name); // 访问alloc对象:拷贝指针 到block的结构体 中使用
  NSLog(@"%@", arr); // 访问alloc对象:拷贝指针 到block的结构体 中使用

  // 可直接修改的变量:
  globalCount = 1; // 全局变量
  localCount = 2; // 静态局部变量
  obj.name = @"momo"; // 局部变量的属性 (仅修改,而不是重指向)
  [arr addObject:@1]; // 局部可变collection(仅修改,而不是重指向)
  
  // 需要__block修饰,才能修改的
  // 会创建__Block_byref_XX_0结构体包装该变量,将此对象的指针拷贝到block结构体中使用
  blockCount = 3; // 局部基本数据类型
  blockObj = [[MOPerson alloc] init]; // 局部alloc变量
  blockArr = [NSMutableArray array];
};
aBlock();

下面一一看其结构:

1、未捕获变量

​ 我们先来看一下未捕获变量时Block的底层实现和内部结构。先在main函数里写一个简单的Block:

void(^aBlock)(void) = ^{
  NSLog(@"Hello world");
}
aBlock();

​ command+S保存一下,然后打开“终端”(terminal):cd + (拖入mian.m所在文件夹),回车。然后输入:(注:后面main.m是文件名,你如果写在其他文件里,就应该改成其他文件的名字)

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

​ 回车后就会生成main.m文件的.cpp文件,即转换成编译后的C/C++文件,打开查看实现代码。实现代码很多,不过不要方,我们需要看的代码就几行,在最下面:

在这里插入图片描述

​ 如上图一共5个部分,分别是:block的信息、block的结构体、block的实现、block的描述、main函数。

  • block的信息:包含4个信息,其中FuncPtr为block括号里的实现方式
  • block的结构体:由两个结构体组成,分别是block的实现和block的描述。(红色线条标注的)
  • block的实现:即我们写在block中括号里的代码
  • block的描述:仅有两个值,reserved表示保留变量占用的内存大小,Block_size表示Block结构体(__main_block_impl_0)占用的内存大小
  • main函数:定义了block,并通过block的结构体__main_block_impl_0进行初始化,传入了两个参数:第一个参数是block括号里的实现(蓝色线条标注的),第二个参数是block的描述(绿色线条标注的)。最后是block的调用,一目了然。

画了结构图如下:

(因为没有访问任何变量,所以此block是global类型的)

注:另外捕获全局变量时,block的结构跟没有捕获时一样。全局变量直接在实现方法里使用,不需要捕获。

2、捕获基本数据类型

​ 写一个使用基本数据类型的block如下:

int a = 1;
void(^aBlock)(void) = ^{
  NSLog(@"%d", a);
}
aBlock();

​ 继续使用terminal转换为底层实现代码如下:

在这里插入图片描述

​ 我们现在只看跟上一个(不捕获变量时)的区别:首先block的结构体变了,多了需要捕获的变量,其初始化方法也多个一个参数,用来传入需要捕获的值。

​ 其结构图如下:

3、捕获alloc变量

​ 写一个使用alloc变量的block如下:

NSObject *obj = [[NSObject alloc] init];
void(^aBlock)(void) = ^{
  NSLog(@"%@", obj);
}
aBlock();

​ 继续使用terminal转换为底层实现代码如下:

在这里插入图片描述

其结构图如下:

​ 可以看到当使用了alloc对象时,跟之前的对比:block的描述(Desc)会多出两个方法:copy和dispose。

  • copy:其内部调用了__Block_object_assign实现对变量的引用方式,会根据变量的类型实现不同的引用方式,在block初始化的时候调用
  • dispose:其内部调用了__Block_object_dispose实现对变量的释放,在block销毁的时候调用

assgin和dispose:最后一个参数表示变量的引用方式,大致有以下几种,分别用来描述不同类型的变量:

enum {
  BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
  BLOCK_FIELD_IS_BLOCK = 7, // a block variable
  BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
  BLOCK_FIELD_IS_WEAK = 16, // declared __weak
  BLOCK_BYREF_CALLER = 128, // called from byref copy/dispose helpers
};	

_Block_object_assign函数会根据所指向对象的修饰符(__strong__weak__block__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。

4、捕获静态局部变量

​ 写一个使用静态局部变量的block如下:

static NSString *name = @"mo";
void(^aBlock)(void) = ^{
  NSLog(@"%@", name);
}
aBlock();

​ 继续使用terminal转换为底层实现代码如下:

在这里插入图片描述

结构如下图:

​ 跟上一个捕获alloc变量对比,捕获静态变量使用的是 指向指针的指针,所以可以直接使用或修改静态变量的值。(捕获局部静态变量的block也是global类型的,因为局部静态变量也是存放在全局区的)

5、__block捕获变量

​ 默认情况下,为块所捕获的变量,是不可以在块里修改的(除了全局变量和静态局部变量)。像上述的2(捕获基本数据)和3(捕获alloc对象)捕获的变量是无法在block内部修改的(只能读值),若想要在block内部修改这样的变量,需要用__block进行修饰后使用,例如:

__block int a = 1;
void(^aBlock)(void) = ^{
  NSLog(@"%d", ++a);
}
aBlock();

​ 继续使用terminal转换为底层实现代码如下:
在这里插入图片描述

结构如下图:

​ 跟之前的对比,可以看到当用__block修饰变量时,系统会为这个变量创建一个结构体__Block_byref_(变量名)_0,包装需要捕获的变量。结构体包含的主要内容:

  • __forwarding:是指向__Block_byref_(变量名)_0结构体的指针,结合block结构体的初始化方法可以看出,将__Block_byref_(变量名)_0结构体a的__forwarding赋值给了block结构体属性a。
  • a(变量名):真正使用到的变量,使用的时候用:a->__forwarding->a

​ 当经过了block的初始化后,__Block_byref_a_0结构体被拷贝到堆上,具体底层怎么拷贝怎么赋值的这里不深究:

  • 为啥不直接使用a->a,而是a->__forwarding->a呢?

    这是因为,如果变量在栈上,就可以直接访问,但是如果已经拷贝到堆上,访问的时候还去栈上访问就会出现问题,所以根据__forwarding找到堆上的地址,然后再使用:

__block拷贝的是 指针对象的内存地址(修饰之前拷贝的是指针)

想改的是:指针的重指向

而不是:指向对象的内容

​ 所以以下几种情况是可以的:

NSMutableString *str = [NSMutableString stringWithString:@"mo"];
NSMutableArray *arr = [NSMutableArray arrayWithArray:@[@1, @2]];
void(^aBlock)(void) = ^{
  str.string = @"moxiaoyan"; // 修改内容(可以)
  // str = [NSMutableString stringWithString:@"moxiaoyan"]; // 重定向(报错)
  [arr addObject:@3];  // 修改内容(可以)
  // arr = [NSMutableArray arrayWithArray:@[@1, @2, @3]]; // 重定向(报错)
  // ... 等等
}
  • 为什么Block不能直接修改外部变量的值呢?
    Apple这样设计,应该是考虑到了block的特殊性,block 本质上是一个对象,block 的花括号区域是对象内部的一个函数,变量进入 花括号,实际就是已经进入了另一个函数区域—改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。
    所以 Apple 在编译器层面做了限制,如果在 block 内部试图修改 auto 变量(无修饰符),那么直接编译报错。
    你可以把编译器的这种行为理解为:对 block 内部捕获到的 auto 变量设置为只读属性—不允许直接修改。

注:因而__block不能修饰全局变量和静态变量(因为这些变量不需要)

四、Block导致循环引用

​ 我们知道当两个对象互相强引用(即:循环引用)时,就会导致内存泄露。block就很容易导致循环引用,如:当前ViewController拥有一个block的属性(VC强引用block),当我们需要在block花括号里使用self时,就会导致block对self的强引用,从而导致循环引用。

1、MRC时代

​ ARC模式还没出来的时候,只能用__unsafe_unretained__block来防止循环引用,如下:

__unsafe_unretained typedof(self) unSafeSelf = self;
void (^aBlock) = ^{
  [unSafeSelf someMethod]; // 不安全,应该判一下空(就很麻烦了)
}
aBlock();
__block typedof(self) blockSelf = self; // 强引用
void (^aBlock) = ^{
  [blockSelf someMethod];
  blockSelf = nil; // 将该强引用置空(这一句不能少)
}
aBlock(); // block必须调用一次,否则blockSelf无法释放

​ 都比较麻烦,不方便~

2、ARC时代

​ ARC模式出来后就有了weak,这时应该使用__weak来避免循环引用:

__weak typedof(self) weakSelf = self; // 弱引用
void (^aBlock) = ^{
  // [weakSelf someMethod]; // 直接使用是有风险的,当此方法执行到一半时,若self被销毁,则会导致crash
  __strong typedof(weakSelf) strongSelf = weakSelf; // 强引用一下(此强引用指针作用域只在该block内)
  [strongSelf someMethod]; // 若此时self已被释放,则给nil发送消息不会报错
  						   // 若此时self未被释放,则strong指针会保证其引用计数在block执行完之前大于0
  // block执行完,strong指针会被释放
}
aBlock(); // 不是必须得执行,因为block里是弱引用

五、面试题

1、__block修饰基本数据类型后其内存地址?

​ 如下代码,a在block代码之前的地址、在block内部的地址、在block代码之后的地址,都一样么?

__block int a = 10;
NSLog(@"%p", &a);
void(^aBlock)(void) = ^{ // a从stack拷贝到heap
  NSLog(@"%p", &a);
};
NSLog(@"%p", &a);
aBlock();

​ 结果是:在block代码之前是一个地址,block代码之后和之内的是另一个地址。因为block初始化的时候会把在栈区的a的结构体拷贝到堆区,访问也都是a->forwarding->a,因而地址变了。

2、block里弱引用和强引用调用方法的区别?

​ 问Block里面 [weakSelf method] 和 [strongSelf method] 的区别?

__weak typedof(self) weakSelf = self; // 弱引用
void (^aBlock) = ^{
  __strong typedof(weakSelf) strongSelf = weakSelf; 
  [weakSelf someMethod]; 
  [strongSelf someMethod]; 
}

​ 用weakSelf:当someMethod执行到一半self突然被释放会crash

​ 用strongSelf:会等block执行完之后,self才会被释放(上面 四-2.ARC时代也提到了)

3、Block从栈拷贝到堆的时机?
  • block调用copy函数时
  • block作为函数返回值时
  • block赋值给__strong修饰的id类型/block类型成员变量时
  • block作为Cocoa API中的方法含有usingBlock的方法参数时
  • block作为GCD API的方法参数时
  • block访问了alloc对象时
4、Block和方法捕获参数的区别?

​ Block捕获参数上面已经说了。方法捕获参数分两种:

  • 传值:

    形参和实参占不同内存单元,传递的实际上是实参变量的一个拷贝副本,形参的值发生变化也不会传回给实参,是单向传递。

  • 传地址:

    传递的是实参变量地址的拷贝值,而不是实参变量的值,在函数中对地址所指对象的操作会改变实参的值。但是形参的内容(即存放的实参变量地址)并不会改变。

5、Block跟函数指针有什么联系?

​ Block是包装了函数指针的对象

6、Block里面使用成员变量会怎样

​ 实际上是:self->(成员变量名),会导致强引用

参考:

OC中block的底层实现原理 (参考结构图)

重识Objective-C:Block底层实现 (参考reserved属性意义)

iOS底层原理总结 - 探寻block的本质(一)(参考结构图)

iOS 底层原理总结:探寻 block 的本质(二)(assign和dispose函数)

Clang Documentation(__block引用变量类型枚举)

iOS中__block 关键字的底层实现原理(不能直接改变值的原因)

iOS-Block底层实现原理(解决循环引用)

说说OC参数传递的那些坑(方法传参)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小莫同学~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值