美文网首页Object-c
iOS Objective-C Block简介

iOS Objective-C Block简介

作者: just东东 | 来源:发表于2020-11-25 14:21 被阅读0次

    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简介

        本文链接:https://www.haomeiwen.com/subject/lokniktx.html