深入理解Objective-C中的Block

block是将函数及其执行上下文封装起来的对象
Objective-C的函数式编程也是通过Block实现的,Block的作用与函数类似,其使用更加灵活,可以像变量一样进行传递。
Block的定义与函数非常类似,一个完整的block结构如下:
ReturnType (^name)(params)
其中ReturnType为返回值的类型,name为Block变量的名字,params为参数列表。
例如:

NSInteger num = 3;
NSInteger (^block)(NSInteger) = ^NSInteger(NSInteger n) {
    return num * n;
};
block(2);

通过clang -rewrite-objc BlockTest.m命令编译该.m文件,发现该Block被编译成如下形式:

NSInteger num = 3;
NSInteger (*block)(NSInteger) = ((NSInteger (*)(NSInteger))&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, num));
((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2);

其中BlockTest是文件名,test是方法名,可以忽略。
__BlockTest__test_block_impl_0结构体为:

struct __BlockTest__test_block_impl_0 {
  struct __block_impl impl;
  struct __BlockTest__test_block_desc_0* Desc;
  NSInteger num;
  __BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, NSInteger _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__block_impl结构体为:

struct __block_impl {
  void *isa; // isa指针,所以Block是对象
  int Flags;
  int Reserved;
  void *FuncPtr; // 函数指针
};

Block内部则为:

static NSInteger __BlockTest__test_block_func_0(struct __BlockTest__test_block_impl_0 *__cself, NSInteger n) {
  NSInteger num = __cself->num; // bound by copy

        return num * n;
    }

所以说Block是将函数及其执行上下文封装起来的对象,既然Block内部封装了函数,那么它同样也有参数和返回值。

一、Block类型

使用下面的代码可以将block对象所属的类以及继承链中的类打印出来:

- (void)viewDidLoad {
    [super viewDidLoad];
    void (^b) (void) = ^() {
        NSLog(@"Block");
    };
    b();
    
    id c = [b class];
    while (c) {
        NSLog(@"%@", c);
        c = [c superclass];
    }
}

控制台输出如下:

2021-09-04 16:02:29.204551+0800 MyProject[4277:94703] Block
2021-09-04 16:02:29.204679+0800 MyProject[4277:94703] __NSGlobalBlock__
2021-09-04 16:02:29.204818+0800 MyProject[4277:94703] NSBlock
2021-09-04 16:02:29.204928+0800 MyProject[4277:94703] NSObject

可以发现,Block最终是继承自NSObject,与普通的Objective-C对象并无本质区别。

  • Block分为全局Block(_NSConcreateGlobalBlock)、栈Block(_NSConcreateStackBlock)、堆Block(_NSConcreteMallocBlock)三种类型。
    其中全局Block存储在已初始化数据(.data)区,栈Block存储在栈(stack)区,堆Block存储在堆(heap)区。

栈Block被存放在内存区域中的栈区。当一个作用域结束后,与之相关的栈中的数据都会被清理,因此对于栈Block,超出了其所在的作用域就被会回收。
堆Block与Objective-C对象一样,内存是否释放会受到引用计数的管理。当对一个栈Block进行copy操作时,就会创建出堆Block。
在不同场景创建的Block,其类型也会不同。

1.不使用外部变量的Block是全局Block

不管Block对象本身是局部的还是全局的,只要没有使用外部变量,即为全局Block,例如:

@implementation ViewController

int count = 10;
void (^b) (void);

- (void)viewDidLoad {
    [super viewDidLoad];
    static NSString *s = @"hello";
    b = ^() {
        NSLog(@"%d, %@", count, s);
    };
    b();
}

@end
2.使用外部变量并且未进行copy操作的Block是栈Block

例如:

NSInteger num = 5;
NSLog(@"%@", [^{
    NSLog(@"block test:%zd", num);
} class]);

输出__NSStackBlock__
日常开发,常用于如下情况:

[self testWithBlock:^{
    NSLog(@"%@", self);
}];

- (void)testWithBlock:(dispatch_block_t)block {
    block();
    
    NSLog(@"%@", [block class]);
}

如果在Block内部有使用到局部变量,则此时创建的Block为栈Block。注意,在ARC环境下,编译器会自动对栈Block进行复制操作,使其变成堆Block,可以在MRC环境下测试。


ARC-Setting.png

将Xcode的编译选项修改为非ARC环境,编写如下测试代码:

@implementation ViewController

void (^b) (void);

- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *s = @"hello";
    b = ^() {
        NSLog(@"%@", s);
    };
    b();
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    b();
}

@end
3.对栈Block进行copy操作,就是堆Block

示例如下:

NSInteger num = 5;
// 赋值即进行copy操作
void(^block)(void) = ^{
    NSLog(@"block test:%zd", num);
};
NSLog(@"%@", [block class]);

输出为__NSMallocBlock__
在MRC环境下运行上面的代码会产生野指针异常,由于我们在创建Block对象时使用了局部变量string,因此该Block会被存储在栈内。当viewDidLoad方法结束时,其中的变量都会被回收,此栈Block也会销毁,虽然使用的全局指针,但是此时指针变成野指针,在viewWillAppear方法中再次调用该Block会产生异常。
要解决上面的野指针问题其实也非常容易,只需要对栈Block进行一次copy操作即可(ARC环境下,编译器会帮助我们做这个copy操作),此时Block就变成堆Block,例如:

@implementation ViewController

void (^b) (void);

- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *s = @"hello";
    b = [^() {
        NSLog(@"%@", s);
    } copy];
    b();
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    b();
}

@end

对栈Block进行copy后,栈Block并不会消失,被copy的仍然是栈Block,示例如下:

[self testWithBlock:^{
    NSLog(@"%@", self);
}];

- (void)testWithBlock:(dispatch_block_t)block {
    block();
    dispatch_block_t tempBlock = block;
    NSLog(@"%@, %@", [block class], [tempBlock class]);
}

输出如下:

2021-10-21 16:06:39.463203+0800 Test[5562:75981] <ViewController: 0x7fd74880ba80>
2021-10-21 16:06:39.463333+0800 Test[5562:75981] __NSStackBlock__, __NSMallocBlock__

需要注意的是,如果对全局Block进行copy操作,得到的仍然是全局Block。
总结:
对栈Block进行copy,将会copy到堆区;
对堆Block进行copy,将会增加其引用计数;
对全局Block进行copy,因为已经是初始化的,所以不做任何处理,仍是全局Block。

二、Block中变量的截获

在Block中如果使用到了外部的变量,则会对外部的变量进行截获。在不同的场景下,Block对变量的截获方式也会不同。
如果在Block中使用了全局变量静态变量,Block会直接对其进行访问,并不会做其他额外的操作,例如:

#import "ViewController.h"

int a = 10;
@interface ViewController ()
@property (nonatomic, copy) void (^block) (void);
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%p", &a);
    self.block = ^() {
        NSLog(@"%p", &a);
        a = 20;
    };
    self.block();
    NSLog(@"%d", a);
}

@end

运行上面的代码,控制台打印如下:

2021-10-20 15:54:17.231457+0800 Test[50301:863187] 0x1095f15a0
2021-10-20 15:54:17.231600+0800 Test[50301:863187] 0x1095f15a0
2021-10-20 15:54:17.231709+0800 Test[50301:863187] 20

从打印信息可以看到,在Block外部和内部使用的全局变量a的地址相同,所以他们实际是同一个变量,在Block内部可以对其进行访问和修改。Block内部并不会对所有用到的变量进行复制,例如全局变量或静态变量就不会复制。
如果在Block中使用到了局部变量(也称为自动变量),则Block会对其进行复制:如果所使用到的变量是值类型的,则会直接复制值;如果所使用的变量是引用类型的,则会复制引用。示例如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int a = 10;
    NSArray *array = @[[NSObject new]];
    NSLog(@"%p, %p", &a, &array);
    self.block = ^() {
        NSLog(@"%p, %p", &a, &array);
    };
    self.block();
}

运行代码,控制台输出如下:

2021-10-20 16:04:30.045456+0800 Test[50653:871056] 0x7ffeed28304c, 0x7ffeed283040
2021-10-20 16:04:30.045604+0800 Test[50653:871056] 0x600001c26488, 0x600001c26480

在Block内部尝试对外部的局部变量进行修改时,会产生编译错误,这是编译器为我们提供的一种错误预警,因为在Block内部的变量已经和外部的变量不再是同一个变量。注意,这里说的不可修改是指变量本身,对于变量引用的对象,如果对象可以修改(例如NSMutableArray类型),在Block内部依然可以对其进行操作。
在ARC环境下,栈Block会被自动复制成堆Block。在这种情况下,被截获的指针变量也会根据修饰符的情况进行内存管理,对于__weak修饰的变量,Block内部不会对其进行强引用,它的引用计数不变,但当外部变量被释放后,Block内部的变量也将不可用。对于__strong修饰的变量,则Block内部会对其也进行强引用,增加其引用计数,当Block对象本身被释放时,其内部截获的这类变量也都会被调用release和释放。

1.局部变量截获,是值截获

例如:

NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n) {
    return n*num; // 已经截获到num值为3,相当于 return n*3
};
num = 1; // 对于Block内部的num无效
NSLog(@"%zd", block(2));

这里输出的是6而不是2,原因就是对局部变量num的截获是值截获。

NSMutableArray *array = [NSMutableArray arrayWithObjects:@"1", @"2", nil];
void(^block)(void) = ^{
    NSLog(@"%@", array);
    [array addObject:@"4"];
};
[array addObject:@"3"]; // 值截获,此处值变了
array = nil; // 值截获,置nil无效,其他修改也无效,如:array = [NSMutableArray arrayWithObjects:@"5", @"6", nil];
block();

打印结果为(
1,
2,
3
)
局部对象变量也是一样,截获的是值,而不是指针,在外部将其置nil,对Block没有影响,而该对象调用方法会影响。

2.局部静态变量截获,是指针截获
static NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n) {
    return n*num;
};
num = 1;
NSLog(@"%zd", block(2));

输出为2,意味着num = 1这里的修改是有效的,即指针截获。同样,在block内部去修改变量num,也是有效的。

3.全局变量、全局静态变量,不截获,直接取值
static NSInteger num3 = 3;

NSInteger num4 = 4;

- (void)test {
    NSInteger num1 = 1;
    static NSInteger num2 = 2;
    __block NSInteger num5 = 5;
    
    void(^block)(void) = ^{
        NSLog(@"%zd", num1); // 局部变量
        NSLog(@"%zd", num2); // 静态变量
        NSLog(@"%zd", num3); // 全局静态变量
        NSLog(@"%zd", num4); // 全局变量
        NSLog(@"%zd", num5); // __block修饰变量
    };
    block();
}

将上面代码用clang编译如下:

struct __BlockTest__test_block_impl_0 {
  struct __block_impl impl;
  struct __BlockTest__test_block_desc_0* Desc;
  NSInteger num1; // 局部变量
  NSInteger *num2; // 静态变量
  __Block_byref_num5_0 *num5; // by ref // __block修饰变量
  __BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, NSInteger _num1, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num1(_num1), num2(_num2), num5(_num5->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

impl.isa = &_NSConcreteStackBlock;这句说明,该block是栈block。
可以看到局部变量被编译成值形式,而静态变量被编译成指针形式,全局变量并未截获。而__block修饰的变量也是以指针形式截获的,并且生成了一个新的结构体对象:

struct __Block_byref_num5_0 {
  void *__isa;
__Block_byref_num5_0 *__forwarding;
 int __flags;
 int __size;
 NSInteger num5;
};

该对象有个属性:num5,即我们用__block修饰的变量。
这里__forwarding是指向自身的(栈block)。
一般情况下,如果我们要对block截获的局部变量进行复制操作,需要添加__block修饰符,而对全局变量、静态变量是不需要添加__block修饰符的。
另外,block里访问self或成员变量都会去截获self。

三、__block关键字

在Block内部对外部的局部变量本身进行修改是不被允许的,如果想要在Block内部修改外部的局部变量,只需要将局部变量声明成__block类型即可,示例如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block int a = 10;
    self.block = ^() {
        a = 20;
    };
    self.block();
    NSLog(@"%d", a); // 20
}

对于使用__block修饰的变量,实际上会被包装成对象,这种操作有些类似于使用指针实现对值类型变量的修改,使用下面的代码也可以实现Block内部修改外部局部变量的值:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int *a = malloc(sizeof(int));
    *a = 10;
    NSLog(@"%p, %p", a, &a);
    self.block = ^() {
        *a = 20;
        NSLog(@"%p, %p", a, &a);
    };
    self.block();
    NSLog(@"%d", *a);
    free(a);
}

运行代码,控制台输出如下:

2021-10-20 16:18:02.356189+0800 Test[51124:880533] 0x600000c04600, 0x7ffee7f31058
2021-10-20 16:18:02.356360+0800 Test[51124:880533] 0x600000c04600, 0x60000001e510
2021-10-20 16:18:02.356484+0800 Test[51124:880533] 20

__block变量在copy时,由于__forwarding的存在,栈上的__forwarding指针会指向堆上的__forwarding变量,而堆上的__forwarding指针指向其自身,所以,对__block的修改,实际上是在修改堆上的__block.
所以,__forwarding指针存在的意义就是,无论在任何内存位置,都可以顺利访问同一个__block变量
另外,由于block捕获的__block修饰的变量会去持有变量,那么如果用__block修饰self,且self持有block,并且block内部使用到__block修饰的self时,就会造成多循环引用,即self持有block,block持有__block变量,而__block变量持有self,造成内存泄露。
例如:

__block typeof(self) weakSelf = self;
_testBlock = ^{
    NSLog(@"%@", weakSelf);
};
_testBlock();

要解决这种循环引用,可以主动断开__block变量对self的持有,即在block内部使用完weakSelf后,将其置nil。但是这种方式有个问题,如果block一直不被调用,那么循环引用将一直存在。
所以,最好还是用__weak来修饰self。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容