iOS Objective-C Block简介

iOS Objective-C Block简介

1. 基本概念

block:带有自动变量(局部变量)的匿名函数(Anonymous function),也被称为闭包(closure),但是本文并不会提及Swift中的闭包。BlockObjective-C对于闭包的实现。Block不仅可以被用作属性还以用作参数和返回值,其实Block就是一个代码块,可以作为变量使用。

  • Block的本质是个对象,可以是代码高度聚合
  • Block可以嵌套定义,定义Block方法和定义函数方法相似
  • Block可以定义在方法内部或外部
  • 只有调用Block的时候,才会执行其{}体内的代码

1.1 Block的定义

Objective-CBlock主要由标识符^、返回值类型、参数列表和代码块组成,可以没有返回值和参数。

^返回值类型(参数列表){代码块}

按照返回值和参数,Block的使用组合有如下四种:

    1. 无返回值 无参数
void(^myBlock)(void) = ^void(void) {
    NSLog(@"无返回值 无参数");
};
// 可以简写成:
void(^myBlock)(void) = ^ {
    NSLog(@"无返回值 无参数");
};
    1. 有返回值 无参数
int(^myBlock)(void) = ^int(void) {
    NSLog(@"有返回值 无参数");
    return 10;
};
// 可以简写成:
int(^myBlock)(void) = ^int {
    NSLog(@"有返回值 无参数");
    return 10;
};
    1. 无返回值 有参数
void(^myBlock)(int a) = ^void(int num) {
    NSLog(@"无返回值 有参数---%d",num);
};
// 可以简写成:
void(^myBlock)(int a) = ^(int num) {
    NSLog(@"无返回值 有参数---%d",num);
};
    1. 有返回值 有参数
int(^myBlock)(int a, int b) = ^int(int a, int b) {
    NSLog(@"无返回值 有参数---%d----%d",a,b);
    return a + b;
};

当返回值和参数为void时我们都可以省略不写,在开发中我们可以使用typedef定义block,在属性中使用copy修饰block

typedef void(^MyBlock)(int a, int b);
@property(nonatomic, copy) MyBlock myBlock;

    self.myBlock = ^(int a, int b) {
        NSLog(@"a + b = %d",a + b);
    };
    
    self.myBlock(10, 20);

1.2 Block与外界变量的关联

1.2.1 Block 使用外界变量

首先来看一个例子

@property(nonatomic, copy)  void(^myBlock)(int a, int b);

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int c = 1;
    self.myBlock = ^(int a, int b) {
        NSLog(@"a + b + c = %d",a + b + c);
    };
    
    c = 2;
    self.myBlock(10, 20);
}

在上面的代码中我们在Block中使用变量c去做计算,但是在执行Block代码前,我们修改了变量c的值,那么Block内部会打印什么呢?我们运行后得到如下结果:a + b + c = 31,由此可知Block在获取外界变量的时候是拷贝了一份,此时无论外界在怎么修改,在Block内部的变量都是不变的。

1.2.1 Block 使用外界变量

如果我们想在Block内部修改外界变量编译器就会报错:

Variable is not assignable (missing __block type specifier)译文:(变量不可以被分配使用,因为缺少__block修饰符)

根据提示,我们给变量c加上__block修饰符,编译器就不会有错误提示了。示例代码如下:

@property(nonatomic, copy)  void(^myBlock)(int a, int b);

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block int c = 1;
    self.myBlock = ^(int a, int b) {
        c = 3;
        NSLog(@"a + b + c = %d",a + b + c);
    };

    self.myBlock(10, 20);
}

此时打印结果为:a + b + c = 33

此时无论我们是在外面还是在Block内部修改变量c的值都会使内外的值保持一致。此处就不在上更多的示例代码了。

Block对于用__block修饰的的外部变量的引用,实际是复制其引用地址来实现访问的,所以Block也就可以修改__block修饰的外部变量的值了。

1.3 Block循环引用

当我们使用Block的时候最常见的问题就是循环引用了,因为Block经常作为属性被self持有,当我们在Block内部使用self的时候就会造成循环引用,如果在代码中造成了循环引用,编译器会报如下的警告:

Capturing 'self' strongly in this block is likely to lead to a retain cycle
译文:Block中强引用了self可能会造成循环引用。

那么该如何解决循环引用呢?下面我们来列举几种解决循环引用的方法:

1.3.1 __weak

weakSelf是我们常用的解决Block循环引用的方法,因其简单方便,深受广大开发者喜欢。示例代码:

@property (nonatomic, copy)  void(^myBlock)(void);
@property (nonatomic, copy) NSString *name;

- (void)test1 {
    __weak typeof(self) weakSelf = self;
    self.name = @"test1";
    self.myBlock = ^{
        NSLog(@"%@",weakSelf.name);
    };
    
    self.myBlock();
}

此处的原理是weakSelf弱引用了selfself持有BlockBlock内部持有weakSelf,因为weakSelfself的持有是弱引用,只是一个指针指向,并不会增加引用计数,此时就会打破循环引用。

weakSelf-->self——>Block——>weakSelf

1.3.2 __weak + __strong

这也是我们常用的一种解决循环引用的方式,那么就会有人想问,不是弱引用就好了吗?为什么会用到strong,这就是weakSelf的坑点了,因为我们使用的weakSelf弱引用,那么就要注意对象的释放时机了,weakSelf对对象是弱引用,如果引用计数为0就会释放对象,但是我们还想让weakSelf持有的对象做一些事情,比如说打印,那么就会造成打印为空的现象,所以这时候使用strongSelf就可以完美的解决这个问题了,示例代码:

@property (nonatomic, copy)  void(^myBlock)(void);
@property (nonatomic, copy) NSString *name;

- (void)test2 {
    __weak typeof(self) weakSelf = self;
    self.name = @"test2";
    self.myBlock = ^{
        
        __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
    };
    
    self.myBlock();
}

在以上代码中,如果我们在一个UIViewController中调用该方法,仅使用weakSelf,在延迟时间没到前就pop回去,就会造成打印为空的情况,因为VC被释放,所以name也就没有值了。在这里我们使用strongSelf即使在延迟时间没到前pop回去,也会保证name的正确打印,并在打印后正常销毁控制器。

此处的使用strongSelfweakSelf做了强引用,但是这个强引用是在Block内部的,作用域只是Block内部,当Block执行完毕自然也就会释放了。

weakSelf-->self——>Block-->(局部变量)strongSelf——>weakSelf

1.3.3 不直接使用self

既然使用self会造成循环引用,那么我们就不用self

@property (nonatomic, copy)  void(^myBlock)(void);
@property (nonatomic, copy) NSString *name;

- (void)test3 {
    __block ViewController *vc = self;
    self.name = @"test3";
    self.myBlock = ^{
        NSLog(@"%@",vc.name);
        vc = nil;
    };
    
    self.myBlock();
}

- (void)test4 {
    __block ViewController *vc = self;
    self.name = @"test4";
    self.myBlock = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
            vc = nil;
        });
    };
    
    self.myBlock();
}

这里我们通过创建一个ViewController的对象,指向self,使用__block进行修饰,在Block内部使用完将其置空,就不会引用着self了,也就解决的循环引用的问题。

vc——>self——>Block vc = nil 时此引用已经断开了。

1.3.4 将self作为参数

此处跟1.3.3中的有异曲同工之妙,既然不能用self那我们也可以传入self,此处需定义一个有参数的Block。示例代码如下:

@property (nonatomic, copy)  void(^mmyBlock)(ViewController *vc);
@property (nonatomic, copy) NSString *name;

- (void)test5 {
    self.name = @"test5";
    self.mmyBlock = ^(ViewController *vc) {
        NSLog(@"%@",vc.name);
    };
    
    self.mmyBlock(self);
}

- (void)test6 {
    self.name = @"test6";
    self.mmyBlock = ^(ViewController *vc) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    
    self.mmyBlock(self);
}

此时我们将self作为参数传入Block,参数在使用完毕后也就销毁了,所以并不会造成循环引用。

1.4 Block的种类

有时候面试官会问你,iOS中有几种block,这个时候如果你回答说Block还有几种?那只能回家等消息了,如果你说三种,那说明你对Block有些研究,如果你能回答6种,那么面试官会继续跟你好好的聊聊。

其实我们常用的Block就是三种,另外三种都是系统级别的Block,一般很少用。这6种Block可以在Apple Opensource中的libclosure源码中的data.c文件中看到。这里推荐一下LGCoocilibclosure-74-KCBuild,可以编译运行的libclosure,可以运行并断点调试Block底层的libclosure-74源码。

void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };

以上就是我们说的6种Block,其中我们常用的Block_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock三种,另外_NSConcreteAutoBlock_NSConcreteFinalizingBlock_NSConcreteWeakBlockVariable三种是系统级别的Block在我们的日常开发中几乎用不到。

1.4.1 _NSConcreteGlobalBlock (NSGlobalBlock)

_NSConcreteGlobalBlock即全局block,不访问外界变量(包括堆区和栈区)

测试代码:

- (void)testGlobalBlock{
    void (^block)(void) = ^{
        NSLog(@"block");
    };
    
    block();
    NSLog(@"%@",block);
}

打印结果:

16057538862814.jpg

1.4.2 _NSConcreteMallocBlock (NSMallocBlock)

_NSConcreteMallocBlock是堆Block,存在于堆内存中,是带一个引用计数的对象,需要自己进行内存管理。变量本身在栈中,因为Block能够自动截获变量,为了访问到变量,会将变量从栈内存中copy到堆内存中。

测试代码:

- (void)testMallocBlock{
    int a = 10;
    void (^block)(void) = ^{
        NSLog(@"block, a的值是:%d", a);
    };
    
    block();
    NSLog(@"%@",block);
}

打印结果:

16057676340000.jpg

1.4.3 _NSConcreteStackBlock (NSStackBlock)

_NSConcreteStackBlock即栈bolck,存储在栈中,目前看来只是一个中间状态了,现在很少有栈Block了,在最新的Xcode12.2中,如果不使用__weak修饰Block是打印不出__NSStackBlock__的。

测试代码:

- (void)testStackBlock{
    int a = 10;
    void (^block)(void) = ^{
        NSLog(@"block, a的值是:%d", a);
    };
    
    NSLog(@"%@",^{
        NSLog(@"block, a的值是:%d", a);
    });
//    block();
//    NSLog(@"%@",block);
}

打印结果:

使用Xcode11.6打印:

16057687328427.jpg

使用Xcode12.2打印:

16057694315008.jpg

同样的代码不同的打印结果,可见由于堆Block的广泛使用,苹果对栈Block应该是在逐步弱化中。

如果你确定要使用栈Block就需要使用__weak进行修饰了,代码如下:

- (void)testStackBlock2{
    int a = 10;
    void (^ __weak block)(void) = ^{
        NSLog(@"block, a的值是:%d", a);
    };
    
    block();
    NSLog(@"%@",block);
}

打印结果:

16057698570134.jpg

当使用__weak修饰Block后,编译器会有个警告⚠️:

Assigning block literal to a weak variable; object will be released after assignment
译文:将块文字赋值给弱变量;对象将在赋值后被释放

此时的警告也在告诉我们,如果这样用会导致Block释放,当然在我们这个例子中不会因释放而导致其他问题,所以如果你特别确认要使用栈Block在使用__weak去修饰,如果不是很确定最好就不要这样做了。

1.4.4 小结

根据上面对常用三种Block的分析我们得出如下结论:

  1. Block默认存储在全局区
  2. 如果Block需要访问外界变量,则需要对Block进行拷贝操作
    1. 首先将Block拷贝到栈区,然后在拷贝到堆区
    2. Xcode12.2以前(未验证,不严谨),如果并没有在Block中使用外界变量前,直接打印Block还是在栈区,在Xcode12.2Block会直接在堆区
    3. 要想使用栈区Block需要使用__weak修饰Block
    4. 可以简单理解为弱引用Block存储在栈区,强引用就要存储在堆区

你可能感兴趣的:(iOS Objective-C Block简介)