前言
对于很多iOS开发者来说,有关block(块)的问题,无论是实际工作,还是面试,被问到的知识点始终都会围绕着以下几点:
- block的定义和使用
- block引用外部变量
(__block 、__week)
- block的内存管理
(堆,栈,全局)
- 另一篇文章主要介绍block的循环引用
(诱因及解决)
下面就会围绕着这几点,进行逐步的分析说明
一、 block的定义和使用
- block的定义
返回值类型(^Block名字)(参数列表);
- block的形式分为一下几种:
1. 无参、无返回值(这里有两种写法,通常使用第二种)
// 返回值类型 (^block名字) (参数列表) = ^(参数列表) {代码块};
void (^DemoBlock)(void) = ^(){
NSLog(@"我是无参数、无返回值的block");
};
DemoBlock(); // 执行block
// 无参数的情况下,`= ^`后面的`()`可以省略 - (常用写法)
void (^DemoBlock)(void) = ^{
NSLog(@"我是无参数、无返回值的block");
};
DemoBlock();
2. 有参、无返回值的block
// 返回值类型 (^block名字) (参数列表) = ^(参数列表) {代码块};
void (^DemoBlock)(int, int) = ^(int a, int b){
NSLog(@"a + b = %d",a + b);
};
DemoBlock(10,10); // 执行block
3. 有参、有返回值
int (^DemoBlock1)(int, int) = ^(int a, int b){
return a + b;
};
int result = DemoBlock1(10,10);
NSLog(@"block返回的结果是 -- %d", result);
4. 无参、有返回值(不常用)
void (^DemoBlock1)(int, int) = ^(int a, int b){
NSLog(@"a + b = %d",a + b);
};
DemoBlock1(10,10);
5. block通常使用typedef
来取别名,例:
// 定义一个带有两个参数的,返回值是int类型的block
typedef int (^DemoBlock)(int , int);
// 这时,DemoBlock就成为了一种类型,可以用这个类型来定义block变量,例:
@property (nonatomic,copy) DemoBlock myBlock;
// 或者
DemoBlock myBlock = ^(int a, int b){
return a + b;
};
可能你想问,为什么@property
后面的修饰符要用copy
,后续再说到block的内存管理时,会特别讲到。
说明:以上重要是为了让大家熟悉block的定义写法,有参数无参数,具体应用具体参考即可
二、 block使用外部变量
如果对上述的block的定义格式很熟悉了,请继续阅读,否则还请先熟练地手写block的定义和使用,会更有所帮助呢...
先来看几个例子,请确认输出结果是什么:
答案可以看过下面的文章之后,然后自己写代码加深印象,会更有助于对block引用外部变量的理解 ^-^
1. 截获自动变量(局部变量)值
block对于外部变量的引用,默认是将变量复制到block的内部,特别要注意的是默认情况下block只能访问不能修改局部变量的值。默认情况下,block引用外部变量,是将变量以const的形式,copy到block内部,因此不允许对const进行修改
int count = 10;
void (^DemoBlock)(void) = ^{
NSLog(@"block内部:count = %d, address = %p",count , &count);
};
count = 20;
NSLog(@"block外部:count = %d, address = %p",count , &count);
DemoBlock();
输出结果:
block外部:count = 20, address = 0x7ffee4093a6c
block内部:count = 10, address = 0x604000254f60
我们发现:
-
block内部
引用了外部的变量
之后,count变量
的地址发生了变化,说明是将count变量
重新copy
了一份在自己的block区域内(由栈区copy到堆区(
- 即使在
block
外部如何对count变量
进行修改,都不会影响block内部
的变量的值(因为两个地址不同,block外的变量仍然是在栈区)
- 如果在block内部对count变量进行修改,则会报如下错误
(在block内,强制将外部的count值修改为30)
2. __block 修饰的外部变量
对于使用__block
修饰的外部变量,block是copy变量的地址到block内部,从而达到可以修改的目的
__block int count = 10;
void (^DemoBlock)(void) = ^{
count = 30; // 这里会报错,要求我们用`__block`来修改外部的count变量
NSLog(@"block内部:count = %d, address = %p",count , &count);
};
NSLog(@"block外部:count = %d, address = %p",count , &count);
DemoBlock();
输出结果:
block外部:count = 10, address = 0x60400003eed8
block内部:count = 30, address = 0x60400003eed8
我们惊奇的发现,一旦使用了__block
修饰的外部变量,不论是在block内不还是外部,变量地址相同,也就从而达到了可以修改的目的。(注意:一旦使用了__block修饰外部变量,这个变量就始终在堆区,而不是在栈区)
三、 block的内存管理
想要了解block内存相关的知识,先要知道程序内存的分配情况,你需要知道:这里主要说明三个区域:
- 全局/静态区:在程序中定义的全局变量、常量存储于此
(不能被修改、随着程序结束才被释放)
- 栈区:在程序中定义的一些局部变量,由系统自动分配和回收,不需要程序员管理
变量出了函数作用域会被系统自动回收并释放内存空间
- 堆区:由程序员主动申请释放,例如alloc、new操作
堆区的内容,是由程序员来控制申请和释放。在程序结束后,系统也会统一回收
- 注⚠️:本文主要讲解关于block的内容,更多关于内存的知识点,有更多的大牛详细介绍,本文只需要了解
堆内存
和栈内存
的特性
即可
那么相应的,block也分为三种
- 全局block(块)
(_NSConcreteGlobalBlock)
:存在于全局内存中, 相当于单例.程序结束的时候才被释放 - 栈block(块)
(_NSConcreteStackBlock)
:存在于栈内存中, 超出其作用域则马上被销毁 - 堆block(块)
(_NSConcreteMallocBlock)
:存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存
那么问题来了,遇到一个Block,我们怎么这个Block的存储位置呢?
1. Block没有访问外界变量(包括栈中和堆中的变量)
Block 既不在栈又不在堆中,在代码段中,ARC和MRC下都是如此。此时为全局块。
2. Block访问外界变量
- MRC 环境下:访问外界变量的 Block 默认存储栈中。
- ARC 环境下:访问外界变量的 Block 默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。
那么你或许想问:在ARC下,访问外界变量的Block为什么要自动从栈区拷贝到堆区呢?(也是算是面试题)
答:这正是由于ARC
的特性,结合栈区
的特点,如同一般的变量一样,出了作用域后,会自动释放掉,如下图
那么
栈block
也是如此。为了保证栈block
不会随着作用域而释放,导致后续无法使用的情况,系统采用这种copy
机制,由栈区
拷贝到堆区
,让程序员来决定何时释放。如图:
特别要说明一点,当使用@property来定义block的时候,这样:
@property (nonatomic,copy) DemoBlock myBlock;
使用的是cpoy
关键字来修饰,原因就是上述所说,DemoBlock
的对象myblock
作为变量,防止在栈区
被释放,需要用copy
,从栈区copy到堆区
PS:本人写了个菜单功能,动态菜单、任意位置弹出。喜欢的点颗小星星、Github怼我