美文网首页iOS-机制
iOS-block(一)-初探

iOS-block(一)-初探

作者: xxxxxxxx_123 | 来源:发表于2020-03-13 16:49 被阅读0次

    基本概念

    什么是block?《Objective-C高级编程》这本书里是这样定义的:

    带有自动变量(局部变量)的匿名函数。顾名思义,匿名函数就是没有名称的函数。也被称为闭包(closure)或者Anonymous function

    我们可以理解为block就是一个没有名称的函数。定义block的方式和定义函数的方式是相似的,而block还可以作为参数使用。当block被调用其块内的代码才会被执行。

    定义

    根据block的定义,我们可以知道,block的主要组成是返回值和参数。其表达式如下:

    ^+返回值类型+参数列表+表达式

    按照是否存在返回值和参数,我们可以将block的定义分为以下几种:

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

    当参数和返回值为void可以忽略。

    在实际开发中,我们经常使用typedef来定义block:

    typedef int(^myBlock)(int a, int b);
    myBlock mb = ^int(int a, int b) {
            return a + b;
    };
    NSLog(@"==myBlock==%d==", mb(1, 2));
    

    block与外界变量的关联

    下面我们来看一个例子:

    typedef int(^myBlock)(int a, int b);
    
    int d = 10;
    myBlock mb = ^int(int a, int b) {
        return a + b + d;
    };
    d = 5;
    NSLog(@"==myBlock==%d==", mb(1, 2));
    

    我们在mb内部使用外部变量int d,随后又给d重新赋值,此时调用mb(1, 2)block内部d的取值是5还是10呢?运行程序,控制台输出13。此例子说明了block具有截获外部变量的能力,而且截获之后,变量在block的值是固定的,不会随着外部的改变而改变。

    我们接着在其外部定义一个变量e:

    int e = 0;
    

    当我们在block内部对e进行赋值操作的时候,编译器会提示错误:

    Variable is not assignable (missing __block type specifier)
    

    意为变量不可以被分配使用,是因为缺少__block修饰符。那么我们为上述变量de的声明加上__block修饰符,我们立刻看到编辑器没有错误了。运行程序,此时控制台输出8,这说明此时block中的取值不再是10而是5了。它不但被我们分配使用了,可以随着外界的改变而改变了,甚至我们可以随意的在block内部修改d的值了,这到底是为什么呢?

    从表面上看,没有被__block修饰的变量,我们在block内部使用的时候,只是截获其当时的值,所以其不会再改变,而被__block修饰的变量,我们截获该变量的地址,所以它不论怎们改变,我们都能截获到。

    block分类

    iOS中,我们依据内存情况将block分为6中

    • _NSConcreteGlobalBlock:全局block,不访问外界变量(包括堆中和栈中的变量)
    void (^block)(void) = ^{
        NSLog(@"==block==");
    };
    
    • _NSConcreteMallocBlock:堆block,存在于堆内存中,是带一个引用计数的对象,需要自己进行内存管理。变量本身在栈中,因为block能够自动截获变量,为了访问到变量,会将变量从堆内存中copy成栈内存中。
    int a = 10;
    void (^block)(void) = ^{
        NSLog(@"==a==%d==", a);
    };
    
    • _NSConcreteStackBlock:栈block,存于栈内存中,超出其作用域则马上进行销毁。作为方法或者函数的参数的时候不会被copy到堆上。
    NSLog(@"%@",^{
        NSLog(@"==block==");
    });
    
    • _NSConcreteAutoBlock
    • _NSConcreteFinalizingBlock
    • _NSConcreteWeakBlockVariable

    前3种在日常开发中是很常见的,后3种是系统级别的block,一般比较少用。

    我们知道程序在编译的时候内存的分布有:堆区(heap)、栈区(stack)、文字常量区、程序代码区、全局区/静态区。

    image

    这张图也就解释了为什么我们使用block作为属性的时候修饰符都是用copy_NSConcreteGlobalBlock是全局block,它只能存在于一个函数的内部,并不能作为属性;而_NSConcreteStackBlock是栈block,存于栈内存中,超出其作用域则马上进行销毁,当我们使用copy就会将其copy到堆内存中,这样会延长block的生命周期,防止出现异常;而_NSConcreteMallocBlock本身就是堆block,使用copy也不会对其有影响。

    block的使用

    block既然是一个匿名函数,那么它就可以作为函数使用,当然,它也可以作为作为函数的参数、或者函数的返回值调用。

    上面的例子大多数都是block作为函数使用,我们也就不再赘述了。下面我们就看看其他两种情况:

    首先我们先给block设置一个别名,这样看着简单明了一些。

    typedef int(^SumBlock)(int a, int b);
    
    1. block作为函数的参数
    - (void)blockAsParameter:(SumBlock)mb {
        NSLog(@"==blockAsParameter实现==%@==", mb);
        mb(1, 2);
    }
    

    可以调用一下,然后运行程序:

    [self blockAsParameter:^int(int a, int b) {
            NSLog(@"==blockAsParameter调用==%d==", a + b);
            return a + b;
    }];
    
    // 控制台输出
    ==blockAsParameter实现==<__NSGlobalBlock__: 0x106a4d090>==
    ==blockAsParameter调用==3==
    
    1. block作为函数的返回值
    - (SumBlock)blockAsReturns {
        NSLog(@"==blockAsReturns==");
        return ^int(int a, int b) {
            return a + b;
        };
    }
    
    SumBlock mb2 = [self blockAsReturns];
    NSLog(@"==blockAsReturns==%d==", mb2(1, 2));
    
    ==blockAsReturns==
    ==blockAsReturns==3==
    

    其实说的通俗点,block就是封装一段代码块,这段代码块可以像变量一样被使用。

    block的循环引用问题

    我们在日常使用block的时候一定要注意一个问题,那就是循环引用。因为block经常是作为变量被self持有,或者是block的持有者被self作为变量持有,然而当我们在block内部使用self的时候就会造成循环引用。我们来看一个例子:

    @property (nonatomic, assign) int num1;
    @property (nonatomic, assign) int num2;
    @property (nonatomic, copy) SumBlock propertyBlock;
    
    self.num1 = 1;
    self.num2 = 2;
    self.propertyBlock = ^int(int a, int b) {
        NSLog(@"==a==%d==b==%d==", self.num1, self.num2);
        return a + b;
    };
    

    编译器会直接提示:

    Capturing 'self' strongly in this block is likely to lead to a retain cycle
    

    意为在block中强引用self可能会造成循环引用。上述例子中,num1num2propertyBlock作为属性被self持有,而我们又在block里使用了num1num2,这就相当propertyBlock又持有了self,这就造成了循环引用。强持有会对引用计数进行处理,循环引用会导致对象无法被释放,就影响了对象的引用计数,会造成内存问题。那么这个问题如何解决呢?

    __weak typeof(self) weakSelf = self;
    self.propertyBlock = ^int(int a, int b) {
        NSLog(@"==a==%d==b==%d==", weakSelf.num1, weakSelf.num2);
        return a + b;
    };
    

    这样就解决了循环引用问题。那么weakSelf是如何解决循环引用的呢?由于现在引入了weakSelf,持有的情况就变成了weakSelf持有selfself持有block,而block又持有weakSelf,但是需要注意的是weakSelf持有self是弱引用,只是一个指向,引用计数并没有发生改变,所以就打破了循环引用。

    但是使用__weak需要注意一点,就是对象的释放时间。

    self.propertyBlock = ^int(int a, int b) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"==a==%d==b==%d==", weakSelf.num1, weakSelf.num2);
        });
    }
    

    如果我们进入页面之后,立即退出页面,控制台输出:

    ==a==0==b==0==
    

    这说明,dealloc之后持有的对象却是已经被释放了。如果我们想等到打印结果输出之后,再进行dealloc该怎么处理。

    1. 使用__strong

    我们可以在block里面使用__strong再将weakSelf转化为强引用即可。此时虽然strongSelf为强引用,但是只是在block作用域内的,当block内任务执行完毕,自然也会释放,和外界并没有任何关系。

    __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"==num==%d==", strongSelf.num);
    });
    
    1. self作为参数传入block

    blcok具有截获变量的能力,当参数传入blockblockcopy一份使用,此时copy出来的变量和原来的就没有关系。这样也打破了循环引用。

    typedef int(^MinusBlock)(int a, int b, ViewController *vController);
    
    self.num = 10;
    self.mb = ^int(int a, int b, ViewController *vController) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"==num==%d==", vController.num);
        });
        return a - b;
    };
    self.mb(2, 1, self);
    

    block的基础就介绍到这里,下一章我们再来看看block的底层分析。

    参考文献:
    Objective-C高级编程 iOSOS X多线程和内存管理》

    相关文章

      网友评论

        本文标题:iOS-block(一)-初探

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