iOS中block技术小结

作者: 点融黑帮 | 来源:发表于2016-12-13 12:58 被阅读360次

    block是C语言级别的语法和运行时特性,应用到Objective-C中可以增强函数功能。在合适场景中灵活应用block技术,对实际开发大有裨益。

    block是对C语言中函数的扩展,除了函数中的代码,还包含变量的绑定。block有时也被称为闭包(closure),闭包就是一个函数,或者一个指向函数的指针,加上这个函数执行的非局部变量。通俗一点,就是闭包允许一个函数访问声明该函数运行上下文中的变量,甚至可以访问不同运行上文中的变量。

    脚本语言:

    function funA(callback){

    alert(callback());

    }

    function funB(){

    var str = "Hello World"; //函数funB的局部变量,函数funA的非局部变量

    funA(

    function(){

    return str;

    }

    );

    }

    通过上面的代码我们可以看出,按常规思维来说,变量str是函数funB的局部变量,作用域只在函数funB中,函数funA是无法访问到str的。但是上述代码示例中函数funA中的callback可以访问到str,就是因为闭包性。

    block实际上就是Objective-C语言对于闭包的实现。block配合dispatch_queue,可以方便地实现简单的多线程编程和异步编程。

    block原型及定义

    block本质上是和其他变量类似。不同的是block存储的数据是一个函数体。使用block时,你可以像调用其他标准函数一样,传入参数,并得到返回值。

    脱字符(^)是block的语法标记,按照我们熟悉的参数语法规约所定义的返回值以及block的主体(也就是可以执行的代码)。下图讲解了如何把block变量赋值给一个变量的语法:

    按照调用函数的方式调用block对象变量就可以了:

    int result = myBlock(4); //result是28

    使用typedef关键字

    由于block数据类型的语法会降低整个代码的阅读性,所以常使用typedef来定义block类型。

    typedef double (^Multiply2BlockRef)(double c, double d);

    这行语句定义了一个名为Multiply2BlockRef的block变量,它包含两个double类型参数并返回一个double类型数值。有了typedef定义,就可以像下面这样使用这个变量:

    Multiply2BlockRef multiply2 = ^(double c, double d) {

    return c * d;

    }

    printf(“%f”, multiply2(4, 5));

    Block和变量

    block被声明后会捕捉创建点时的状态。block可以访问函数用到的标准类型的变量:

    全局变量;

    全局函数(这个是可以调用);

    封闭范围内的变量;

    与block声明时同级别的__block变量(这是可以修改的);

    封闭范围内的非静态变量会被获取为常量;

    Objective-C对象;

    block内部变量;

    3.1 本地变量

    本地变量就是与block在同一范围内声明的变量。

    typedef double (^Multiply2BlockRef)(double c, double d);

    double a = 10, b = 10;

    Multiply2BlockRef multiply = ^(void) { return a * b;}

    a = 20;

    b = 20;

    NSLog(@“%f”, multiply());

    这个NSLog会输出什么值?400?不是,为什么?

    因为对于本地变量,block定义时copy变量的值,在block中作为常量使用,所以即使变量的值在block外改变,也不影响他在block中的值。NSLog只会输出100。

    3.2 全局变量

    全局变量或静态变量在内存中的地址是固定的,block在读取该变量值的时候是直接从其所在内存读出,获取到的是最新值,而不是在定义时copy的常量。

    3.3 Block变量

    本地变量会被block作为常量获取到,如果想要修改它们的值,必须将它们声明为可修改的,否则会编译出错。

    double c = 3;

    Multiply2BlockRef multiply2 = ^(double a, double b){ c = a * b; } // 编译会报错

    如果想要在block代码里修改本地变量,需要将变量标记为__block。基于之前的代码,给变量c添加__block关键字,如下:

    __block double c = 3;

    对于用__block修饰的外部变量引用,block是复制其引用地址来实现访问的。

    3.4Objective-C对象

    一般来说我们总会在设置block之后,在合适的时间回调block,而不希望回调block的时候block已经被释放了,所以我们需要对block进行copy,copy倒堆中,以便后用。

    当一个block被Copy的时候,如果你在block里进行了一些调用,那么将会有一个强引用指向这些调用方法的调用者,有两个规则:

    如果是通过引用来访问一个实例变量,那么将强引用至self;

    如果是通过值来访问一个实例变量,那么将直接强引用至这个“值”变量;

    苹果官方文档里有两个例子来说明这两种情况:

    dispatch_async(queue, ^{

    // instanceVariable is used by reference, a strong reference is made to self

    doSomethingWithObject(instanceVariable);

    });

    id localVariable = instanceVariable;

    dispatch_async(queue, ^{

    /*

    localVariable is used by value, a strong reference is made to localVariable

    (not to self)

    */

    doSomethingWithObject(localVariable);

    });

    上面第一种情况相当于通过self.xxx来访问实例变量,所以强引用指向了self;第二种情况把实例变量变成了本地临时变量,强引用将直接指向这个本地的临时变量。有时第一种情况可能会造成循环引用(在后面的注意事项中会解释),要避免强引用到self的话,用__weak把self重新引用一下,在block中使用weakSelf就行了,比如:

    __weak UIViewController *weakSelf = self;

    Block类型

    block有几种不同的类型,这里列出常见的三种类型:

    _NSConcreteGlobalBlock:全局的静态block,不会访问任何外部变量,不会涉及到任何拷贝,比如一个空的block。例如:

    int main()

    {

    ^{ printf("Hello, World!\n"); } ();

    return 0;

    }

    _NSConcreteStackBlock:保存在栈中的block,当函数返回时被销毁。例如:

    int main()

    {

    char a = 'A';

    ^{ printf("%c\n",a); } ();

    return 0;

    }

    _NSConcreteMallocBlock:保存在堆中的block,当引用计数为0时被销毁。该类型的block都是由_NSConcreteStackBlock类型的block从栈中复制到堆中形成的。例如下面代码中,在exampleB_addBlockToArray方法中的block还是_NSConcreteStackBlock类型的,在exampleB方法中就被复制到了堆中,成为_NSConcreteMallocBlock类型的block:

    void exampleB_addBlockToArray(NSMutableArray *array) {

    char b = 'B';

    [array addObject:^{

    printf("%c\n", b);

    }];

    }

    void exampleB() {

    NSMutableArray *array = [NSMutableArray array];

    exampleB_addBlockToArray(array);

    void (^block)() = [array objectAtIndex:0];

    block();

    }

    小结

    _NSConcreteGlobalBlock类型的block要么是空block,要么是不访问任何外部变量的block。它既不在栈中,也不在堆中,可以理解为它在内存的text区。

    _NSConcreteStackBlock类型的block有闭包行为,也就是有访问外部变量,并且该block有且只有一次执行,因为栈中的空间是可重复使用的,所以当栈中的block执行一次之后就被清除出栈了,所以无法多次使用。

    _NSConcreteMallocBlock类型的block有闭包行为,并且该block需要被多次执行。当需要多次执行时,就会把该block从栈中复制到堆中,供以多次执行。

    使用block的注意事项

    避免在block里用self造成循环引用

    block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增加1(MRC)。在ARC与MRC环境下对block使用不当都会引起循环引用问题。一般表现为,某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身,简单说就是block的这种循环引用会被编译器捕捉到并及时提醒。

    self.someBlock = ^(Type var) {

    [self doSomething];

    //或self.otherVar = XXX;

    //或_otherVar = ...

    };

    即使在block代码中没有显式地出现"self",也会出现循环引用!只要在block里用到了self所拥有的东西!

    对于这种情况,可以通过添加__weak声明(ARC)或者__block声明(MRC)去禁止block对self进行强引用或者强制增加引用计数。

    开发过程中该选择block还是delegate

    如果对象有超过一个以上不同的事件源,使用delegation;一般的delegate方法会有返回值;delegate的回调更多的面向过程,而block则是面向结果的。如果需要得到一条多步进程的通知,应该使用delegation。而只是希望得到请求的信息(或者获取信息时的错误提示),应该使用block。

    总结

    可见灵活安全地使用block,必定会使编码工作事半功倍。block经常被用作回调函数,取代传统的回调方式,可以使得编码时更顺畅,不用中途换到另一个地方写一个回调函数。采用block,可以在调用函数时直接写后续处理代码,将其作为参数传递过去,供其任务执行结束时回调。block通常适用于以下场景:任务完成时回调;处理消息监听回调处理;错误回调处理;枚举回调;视图动画、变换;排序等。

    本文作者:张生辉(点融黑帮),来自点融北京技术团队,曾在索贝做过3年C++开发,目前主要做iOS开发,对前端技术及新兴技术比较感兴趣。

    相关文章

      网友评论

      本文标题:iOS中block技术小结

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