Block的三次探索

作者: iOS_aFei | 来源:发表于2017-04-24 07:30 被阅读358次

    Block是带有自动变量的匿名函数。
    匿名函数的含义是Block没有函数名,另外Block带有插入记号“^”,插入记号便于查找到Block。

    一般形式的Block:

    ^int (int cs) {
        return cs * 2;
    };
    

    如果Block没有参数,参数列表可省略,形式如下:

    ^int{
        return 666;
    };//参数列表的括号也可以省略
    

    在C语言中,函数返回值类型省略则默认为int类型;在Block中也可以省略返回值类型,不过与C语言函数不同,Block省略返回值类型时,如果表达式中有return语句就使用该返回值的类型,没有return语句就使用void类型。
    返回值为void类型时的Block:

    ^{
        printf("iam Block”);
    };
    

    返回值为int类型时的Block:

    ^{
        return 666;
    };
    

    上面定义了Block语句块,要想使用还需要Block类型变量:

    int (^block1)(int) = ^int (int cs) {
        return cs * 2;
    };
    printf("%d",block1(666));
    

    声明Block类型变量:

    int (^block2)(int);
    

    Block类型变量可以作为自动变量、函数参数、静态变量、全局变量使用。以函数参数为例:

    int test(int (^block1)(int)) {
        return 1;
    }
    

    除了看起来长一点也没啥,typedef来解决_

    typedef int (^Block)(int);
    int test(MyBlock block1) {
        return 1;
    }
    

    这样就舒服了、看起来。

    接下来就是高潮部分了,就是”带有自动变量“以及如何带有自动变量?

    int var1 = 10;
    int var2 = 20;
    void (^block3)(void) = ^{
         printf("%d",var1);
    };
    var1 = 20;
    block3();
    

    情况1:打印结果是10,而不是20。
    情况2:在Block中修改var1的值会报错。
    情况3:如果修改的是Objective-C对象,例如NSArray对象,不可以对其赋值但是可以增删元素。
    情况4:对于全局变量、静态全局变量、静态变量在块中可以修改。
    情况5:使用__block修饰var1变量后,打印的结果是20,而且var1可以在块中被修改。
    另外注意:不能在block中访问C语言字符数组,但是可以访问C语言字符串,也就是说Block并没有截获字符数组。

    解释

    要想明白为什么会出现上述情况,我们必须深入了解Block,打开终端进入项目main.m所在的文件夹,使用命令clang -rewrite-objc main.m,执行之后该文件夹下会出现一个main.cpp的C++文件,文件中代码很长,我们只需看重要的一些代码。
    这是一个没有使用局部变量的block:

    #import <Foundation/Foundation.h>
    int main() {
        void (^block)(void) = ^{
            printf("block\n");
        };
        block();
        return 0;
    }
    

    在main.cpp中我们看这些代码:

    struct __block_impl {
        void *isa;   
        int Flags;
        int Reserved;
        void *FuncPtr;
    };//^^^^结构体2^^^^
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;    
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };//^^^^结构体1^^^^
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
            printf("block\n");
        }
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    //^^^^结构体3^^^^
    
    int main() {
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
    }
    

    有三个结构体(代码中已标记)我们必须铭记于心,或许你看到这里就不想往下看了,但是你对一个知识点的理解深度与面试官对你的好感成正比,Block几乎每个iOS面试官都会问到。

    结构体1:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
        
        使用到的一些自动变量加到这个结构体中
        
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    这个结构体包含结构体2和结构体3,另外还有一些在该block中使用到的局部变量,最后是一个构造函数,对结构体的成员(上述结构2、结构3、局部变量)进行初始化。请注意构造函数的第一个参数,下面还会说。

    结构体2:

    struct __block_impl {
        void *isa;      //block也是对象
        int Flags;      //标志
        int Reserved;   //版本升级所需的区域
        void *FuncPtr;  //函数指针
    };
    

    结构体3:

    static struct __main_block_desc_0 {
      size_t reserved;      // 版本升级所需的区域
      size_t Block_size;    // Block的大小
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    

    然后会发现上面还有这个函数:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
            printf("block\n");
        }
    
    

    对于上面这段代码是不是很熟悉,很明显对应于我们定义的block,也就是Block使用的匿名函数被转换为了C语言函数,命名的根据Block所在的函数名和出现的顺序。需要注意的是还有一个struct __main_block_impl_0 *__cself指针,为什么要这样设计Block呢?结合类、对象、方法、自己思考。
    上面说注意结构体1中构造函数的第一个参数,现在看下main函数中void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));就是用上面的函数的地址作为参数的。
    第二个参数时作为静态全局变量传入的,在结构体3最后可以看到使用__main_block_impl_0的大小进行初始化的。
    这段代码就是将栈上生成的变量赋值给block变量。

    上面看了最简单的Block的转换代码,下面来看一个使用到了局部变量的block:

    #import <Foundation/Foundation.h>
    int main() {
        int var1 = 10;
        int var2 = 10;
        void (^block)(void) = ^{
            printf("%d\n", var1);
        };
        block();
        return 0;
    }
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      
      int var1;
      
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _var1, int flags=0) : var1(_var1) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    在结构体1中加入了使用到的局部变量。现在就可以解释情况1-3了:
    情况一和情况二解释:自动变量会以值传递的方式拷贝到Block的结构体中,因为并没有传递自动变量的地址,所以不能修改自动变量的值。
    情况三解释,修改NSArray对象可以,重新赋值则不行,因为修改对象并没有改变对象指针。

    下面再来说说__block说明符:
    通过上面的方法可以对非静态全局变量、静态全局变量、静态局部变量、__block变量进行测试,可以得到:
    情况四解释:对于访问全局变量和静态全局变量,对于Block的结构体没有任何影响,因为其地址是不变的、作用域也足够广。对于静态局部变量,虽然地址是唯一的,但是Block超出了其作用域,所以将静态局部变量的指针传递给了Block的结构体。
    为什么不将自动变量的地址拷贝到Block中呢?
    自动变量超出作用域之后会被废弃,而Block可能被拷贝到堆上。所以我们看下__block变量的实现。

    struct __Block_byref_var1_0 {
      void *__isa;
    __Block_byref_var1_0 *__forwarding;
     int __flags;
     int __size;
     int var1;
    };
    

    变换后变成了结构体。
    其初始化为:

    __attribute__((__blocks__(byref))) __Block_byref_var1_0 var1 = {(void*)0,(__Block_byref_var1_0 *)&var1, 0, sizeof(__Block_byref_var1_0), 10};
    

    从初始化可以看出:__forwarding指针指向自己(不一定,后面会说),var1变量相当于原自动变量。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_var1_0 *var1 = __cself->var1; // bound by ref
    
            printf("%d\n", (var1->__forwarding->var1));
        }
    

    访问var1时为什么兜圈子呢?后面会说。

    另外还有结构体2中的isa指针,我们知道在类与对象中:对象的isa指针指向所属的类,类的isa指针指向元类,元类的isa指针指向根元类,根元类的isa指针指向自己。
    而Block的isa指针的指向有三种,而且我们暂时给出它们的特点:
    NSConcreteStackBlock:存储在栈上的的Block。特点:使用了局部变量,可以被赋值到堆上。
    NSConcreteMallocBlck:存储在堆上的BlocK。特点:由NSConcreteStackBlock拷贝到堆上,持有对象
    NSConcreteGlobalBlock:存储在程序的数据区。特点:不使用局部变量,整个程序中只需一个实例。

    为什么,第一个事例没有使用局部变量,isa指针却:

    impl.isa = &_NSConcreteStackBlock;
    

    虽然说isa指针指向的是_NSConcreteStackBlock,但它的实现是NSConcreteGlobalBlock,因为它没有使用局部变量。

    设置在栈上的Block超出作用域会被废弃,__block也会被废弃。在以下几种情况Block会被复制到堆上:
    1、调用copy方法(_NSConcreteStackBlock调用copy方法会从栈复制到堆,_NSConcreteGlobalBlock什么也不做,_NSConcreteMallocBlock引用计数加一);
    2、作为函数返回值;
    3、将Blcok赋值给类的成员变量时;
    4、向方法名中含有usingBlock的Cocoa框架方法或GCD的API传递Block时。

    __block变量会跟随Block从栈复制到堆、并被Block持有。栈上的__blcok变量的__forwarding指针也指向堆上的结构体,保证了可以访问同一个__block变量。

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->var1, (void*)src->var1, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->var1, 8/*BLOCK_FIELD_IS_BYREF*/);}
    

    这两个函数就是保证__block变量被复制到堆上被Block持有和释放。
    这个实例中我们使用__block修饰的int类型,如果修饰的对象呢?
    对象有四种修饰符:__strong、__weak、__unsafe_unretained、__autorelealeasing四种修饰符,对于````__strong同样是使用上述的两个函数进行持有和释放。而对于__weak修饰的对象,当超出作用域后被废弃,也就是即使同时使用__block __weak修饰了对象,但是当对象废弃后,Block持有的对象也会变成nil。同理__unsafe_unretained需要注意悬垂指针的问题。最后同时使用__block __autoreleasing```会编译错误。

    上面还曾说过一句话:堆上的Block持有对象,现在说完:堆上的对象会持有__strong对象,而栈上的Blcok不持有。

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    

    当栈上的Block复制到堆上时,会调用这copy函数持有对象,使用dispose函数释放对象。
    Blcok通过参数区分copy和dispose函数的对象类型是对象还是__block变量。

    最后就是Block的循环引用:
    循环引用这里就不在细说,解决方式其一就是使用__weak修饰符来解决。
    现在主要说一下另外一种方式:
    使用__block变量手动置nil:

       __block id tmp = self;
        _myblock = ^{
            [tmp doSomething];
            tmp = nil;
        };
    

    但是需要注意此Blcok必须要的执行

    最后补充:我们在将Blcok从栈copy到堆上时都是使用copy,这是因为栈上的Block使用retain是无效的,只有使用copy函数可以。但是对于在堆上的Block可以通过copy和retain持有,所以还是推荐使用copy。

    对于Block的探索,这不是第一次也绝对不是最后一次。

    相关文章

      网友评论

        本文标题:Block的三次探索

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