美文网首页iOS学习开发
iOS Block从入门到完全详解

iOS Block从入门到完全详解

作者: EdenMa | 来源:发表于2019-12-10 23:12 被阅读0次

    欢迎大家关注我的个人博客:https://darkknightkazuma.github.io

    一、Block基础介绍

    1、概念介绍

    Block又称为块或块对象,它是苹果在OSX10.6和iOS4.0中新加入的功能,是C语言层面的特性及功能实现,类似其它语言的闭包(closure)功能.当时苹果正使用LLVM的clang作为C语言的编译器来改进C/OC/C++/OC++等的编译处理,Block也是当时的重要成果.

    2、块的定义及语法

    Block是带有自动变量(局部变量)的匿名函数.因为底层结构体实现有isa指针,也被看作是块对象.Block的出现其实是为了代替指针函数的一种语法结构,之前传递状态需要使用不透明的void指针来传递状态,而Block把C语言特性所编写的代码封装成简明且易用的接口.

    下面是Block的实现语法结构:
    block的语法是脱字符^加花括号,花括号里是块的实现.块可以当成是变量的值使用.如下:

    ^{
        //Block implementation
    }
    

    Block类型的声明语法结构:

    return_type (^block_name)(parameters)
    //中间的block_name可以看作是变量名
    

    Block的调用:

    block_name(parameters)
    

    二、Block的关于捕获变量的底层实现

    在块声明的范围里,所有变量都可以为其所捕获.捕获的自动变量不可修改,修改要加__block前缀.
    块总能修改实例变、静态变量、全局变量、全局静态变量,无需加__block.

    我们经常会看到block相关的文章或面试题中有这些内容.那么Block底层实现是怎么样的呢?

    1、我们首先来看一下Block的内存布局,如下:
    /* Revised new layout. */
    struct Block_descriptor {
        unsigned long int reserved; //预留内存大小
        unsigned long int size; //块大小
        void (*copy)(void *dst, void *src); //指向拷贝函数的函数指针
        void (*dispose)(void *); //指向释放函数的函数指针
    };
    
    
    struct Block_layout {
        void *isa; //指向Class对象
        int flags; //状态标志位
        int reserved; //预留内存大小
        void (*invoke)(void *, ...); //指向块实现的函数指针
        struct Block_descriptor *descriptor;
        /* Imported variables. */
    };
    

    其中最重要的就是invoke函数指针和descriptor块的描述.invoke函数指针它指向了块的实现,它的void*参数传入的是块的结构体. descriptor的结构体中包含了块大小以及两个重要的辅助函数指针等.我们注意到块的布局中,最下面一部分是捕获到的变量,前面提到的void*参数传入块的结构体也是为了拿到捕获到的变量.那么下面让我们来看一下块的实际源码是什么样的:

    cd到目录下,在终端通过 clang -rewrite-objc 文件名 的方法将文件转换为C/C++代码.如果发现不能转换需要下载插件,指令为xcode-select --install.

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int ivar = 1;
            void (^tmpBlock)(void) = ^{
                NSLog(@"tmpBlock:%d",ivar);
            };
            tmpBlock();
        }
        return 0;
    }
    

    转换后点开目录下的.cpp文件,会看到上万行的代码,屏蔽掉无用的代码,直接找到主要代码如下:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int ivar;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _ivar, int flags=0) : ivar(_ivar) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int ivar = __cself->ivar; // bound by copy
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_459ed6_mi_2,ivar);
            }
    
    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)};
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            int ivar = 1;
            void (*tmpBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, ivar));
            ((void (*)(__block_impl *))((__block_impl *)tmpBlock)->FuncPtr)((__block_impl *)tmpBlock);
        }
        return 0;
    }
    

    我们来挨个看一下:

    __block_impl 结构体是包含我们刚刚在Block布局中提到的前四个成员变量.

    __main_block_impl_0结构体是包含了__block_impl结构体变量和__main_block_desc_0结构体指针以及捕获到的变量.最后是初始化构造函数中对impl成员和desc进行赋值.

    __main_block_func_0这个函数他传递了__main_block_impl_0*型的参数,并从中读取块结构体中捕获的参数进行实际操作.看到这里你有个明白了这个就是对应上面那个invoke函数指针所指向的函数.

    __main_block_desc_0这个结构体就是上面所说的块的描述结构体.可能你会发现他少了两个辅助函数的函数指针,原因后面会说.

    最后是main函数中的具体初始化和函数调用.刚好对应块变量的声明实现部分和块的调用.

    2、关于捕获自动变量的分析

    捕获自动变量这部分首先我们要明确有哪几种变量,如下:

    • auto自动变量:默认方法的作用域中不加前缀就是自动变量,而且ARC下默认还会加__strong.
    • static静态变量:存放在内存的可读写区,初始化只在第一次执行时起作用,运行过程中语句块变量将保持上一次执行的值.
    • static全局静态变量:也放在可读写区,作用域为当前文件.
    • 全局变量:也在可读写区,整个程序可用.
    • 另外在OC中它们又分为对象类型和非对象类型.

    下面让我们看一下他们在Block中的表现.
    首先是前面那个是用自动变量ivar的例子,如果我们修改它的值编译器会报警告:

    Variable is not assignable (missing __block type specifier)
    

    提示我们不能修改变量,这是为什么呢?我们来打印一下block内外的指针变量的地址:

        int ivar = 1;
            NSLog(@"块外%p",&ivar);
            void (^tmpBlock)(void) = ^{
                NSLog(@"块内%p",&ivar);
            };
            tmpBlock();
    
    2019-12-05 18:20:40.063273+0800 LearningDemo[69358:5789849] 块外0x7ffeefbff4dc
    2019-12-05 18:20:40.063840+0800 LearningDemo[69358:5789849] 块内0x103400ad0
    

    你会发现它们不是同一个地址,是的,block的__main_block_impl_0结构体拷贝了一份自动变量进去作为结构体的成员变量,你修改的是结构体内部的ivar的值,而不是外部ivar的值,他们并不是同一块内存上的东西.苹果让编译器在这种情况下报警告,提示开发者是改不了的.
    接下来我们给它加一个static前缀,如下:

            static int ivar = 1;
            NSLog(@"块外%p",&ivar);
            void (^tmpBlock)(void) = ^{
                ivar = 2;
                NSLog(@"块内%p",&ivar);
            };
            tmpBlock();
    
    2019-12-05 18:41:36.083804+0800 LearningDemo[69801:5806136] 块外0x100003320
    2019-12-05 18:41:36.084770+0800 LearningDemo[69801:5806136] 块内0x100003320
    

    有意思的事情发生了,可以修改变量了,而且地址居然一样了~那我们看一下转换后的源码:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *ivar;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_ivar, int flags=0) : ivar(_ivar) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *ivar = __cself->ivar; // bound by copy
    
                (*ivar) = 2;
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_532557_mi_3,&(*ivar));
            }
    
    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)};
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            static int ivar = 1;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_532557_mi_2,&ivar);
            void (*tmpBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &ivar));
            ((void (*)(__block_impl *))((__block_impl *)tmpBlock)->FuncPtr)((__block_impl *)tmpBlock);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_532557_mi_4,tmpBlock);
        }
        return 0;
    }
    

    从上面的代码可以看到,它捕获到的变量变成了指针变量int *ivar,然后再看打印的代码中的&(*ivar),它拿到的是指针变量中所存的地址,也就是说结构体捕获的指针变量存储的地址和Block外部的静态变量是同一个地址!而Block实现的赋值是通过*运算符引用变量来赋值的,像这样(*ivar) = 2;.到这里你应该已经明白为什么可以改变外部变量了吧,因为它们访问的是同一个内存地址.总结一下就是地址访问是可以修改外部变量的. 那你可能会说,对象类型本质上不就是一个指针变量吗,为什么不能修改,我们以NSString*为例看一下它转换后的实现源码:

                NSString * strvar = @"strvar";
                NSLog(@"块外%p",&strvar);
                void (^tmpBlock)(void) = ^{
                    NSLog(@"块内%p",&strvar);
                };
                tmpBlock();
    
    2019-12-05 19:01:07.019544+0800 LearningDemo[69970:5816051] 块外0x7ffeefbff4d8
    2019-12-05 19:01:07.020200+0800 LearningDemo[69970:5816051] 块内0x100701780
    

    从上面的结果可知,地址是不一样的,为什么不一样呢 看一下源码就知道了

     NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_ca150a_mi_4,&strvar);
    

    从源码我们得知他取得的是指针变量的地址,而非指针变量存储的地址,所以我们给对象类型赋值其实是赋值一个新的对象地址上去,而不是通过同一个地址去修改替换分配在堆上的对象. 当我们加上static前缀之后发现可以修改了,其实是因为Block捕获的是NSString **strvar;指针的指针,也就是存的是外部对象类型变量的地址而不是变量内存储的地址,所以我们可以修改strvar了.

                NSMutableString * strvar = [NSMutableString stringWithFormat:@"NSMutableString"];
                NSLog(@"块外%p",&strvar);
                void (^tmpBlock)(void) = ^{
                    [strvar appendString:@"111"];
                    NSLog(@"块内%p",&strvar);
                };
                NSLog(@"tmpBlock %@",strvar);
                tmpBlock();
                NSLog(@"tmpBlock %@",strvar);
    
    2019-12-06 12:01:25.339194+0800 LearningDemo[81031:6194158] 块外0x7ffeefbff4d8
    2019-12-06 12:01:25.339677+0800 LearningDemo[81031:6194158] tmpBlock NSMutableString
    2019-12-06 12:01:25.339739+0800 LearningDemo[81031:6194158] 块内0x102841ef0
    2019-12-06 12:01:25.339780+0800 LearningDemo[81031:6194158] tmpBlock NSMutableString111
    

    你会发现如果是可变对象的话是可以通过方法来修改堆上的对象的,也就是说只要可以通过同一个地址访问到变量就可以对其赋值修改等.

    全局变量静态全局变量因为作用域很广,在它们的作用域里都可以访问,所以并不需要捕获,都可以Block内修改,下面是测试代码:

    int globalVar = 1;
    static int staticGlobalVar = 1;
    NSString *globalStr = @"globalStr";
    static NSString *staticGlobalStr = @"staticGlobalStr";
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            NSLog(@"块外%p",&globalVar);
            NSLog(@"块外%p",&staticGlobalVar);
            NSLog(@"块外%p",&globalStr);
            NSLog(@"块外%p",&staticGlobalStr);
             void (^tmpBlock)(void) = ^{
                 globalVar += 1;
                 staticGlobalVar += 1;
                 globalStr = @"修改globalStr";
                 staticGlobalStr = @"修改staticGlobalStr";
                 NSLog(@"块内%p",&globalVar);
                 NSLog(@"块内%p",&staticGlobalVar);
                 NSLog(@"块内%p",&globalStr);
                 NSLog(@"块内%p",&staticGlobalStr);
             };
            NSLog(@"globalVar: %d",globalVar);
            NSLog(@"staticGlobalVar: %d",staticGlobalVar);
            NSLog(@"globalStr: %@",globalStr);
            NSLog(@"staticGlobalStr: %@",staticGlobalStr);
            tmpBlock();
            NSLog(@"globalVar: %d",globalVar);
            NSLog(@"staticGlobalVar: %d",staticGlobalVar);
            NSLog(@"globalStr: %@",globalStr);
            NSLog(@"staticGlobalStr: %@",staticGlobalStr);
        }
        return 0;
    }
    2019-12-06 13:54:10.126960+0800 LearningDemo[82231:6240152] 块外0x100003308
    2019-12-06 13:54:10.127585+0800 LearningDemo[82231:6240152] 块外0x100003320
    2019-12-06 13:54:10.127687+0800 LearningDemo[82231:6240152] 块外0x100003310
    2019-12-06 13:54:10.127738+0800 LearningDemo[82231:6240152] 块外0x100003318
    2019-12-06 13:54:10.127774+0800 LearningDemo[82231:6240152] globalVar: 1
    2019-12-06 13:54:10.127802+0800 LearningDemo[82231:6240152] staticGlobalVar: 1
    2019-12-06 13:54:10.127847+0800 LearningDemo[82231:6240152] globalStr: globalStr
    2019-12-06 13:54:10.127891+0800 LearningDemo[82231:6240152] staticGlobalStr: staticGlobalStr
    2019-12-06 13:54:10.127970+0800 LearningDemo[82231:6240152] 块内0x100003308
    2019-12-06 13:54:10.128051+0800 LearningDemo[82231:6240152] 块内0x100003320
    2019-12-06 13:54:10.128112+0800 LearningDemo[82231:6240152] 块内0x100003310
    2019-12-06 13:54:10.128170+0800 LearningDemo[82231:6240152] 块内0x100003318
    2019-12-06 13:54:10.128221+0800 LearningDemo[82231:6240152] globalVar: 2
    2019-12-06 13:54:10.128271+0800 LearningDemo[82231:6240152] staticGlobalVar: 2
    2019-12-06 13:54:10.128365+0800 LearningDemo[82231:6240152] globalStr: 修改globalStr
    2019-12-06 13:54:10.128870+0800 LearningDemo[82231:6240152] staticGlobalStr: 修改staticGlobalStr
    

    3、关于_ _block的底层实现原理

    我们理解上面那些内容之后接下来正式讲解__block的原理:

                    __block int ivar = 1;
                    NSLog(@"块外%p",&ivar);
                    void (^tmpBlock)(void) = ^{
                        ivar += 1;
                        NSLog(@"块内%p",&ivar);
                    };
                    ivar += 1;
                    NSLog(@"ivar1: %d",ivar);
                    tmpBlock();
                    NSLog(@"ivar2: %d",ivar);
                    NSLog(@"tmpBlock %@",tmpBlock);
    
    2019-12-06 14:23:05.294076+0800 LearningDemo[82616:6257418] 块外0x7ffeefbff4d8
    2019-12-06 14:23:05.294589+0800 LearningDemo[82616:6257418] ivar1: 2
    2019-12-06 14:23:05.294673+0800 LearningDemo[82616:6257418] 块内0x102c00a78
    2019-12-06 14:23:05.294726+0800 LearningDemo[82616:6257418] ivar2: 3
    2019-12-06 14:23:05.295064+0800 LearningDemo[82616:6257418] tmpBlock <__NSMallocBlock__: 0x102c009f0>
    

    将上面代码转换成底层实现源码:

    struct __Block_byref_ivar_0 {
      void *__isa;  //对象特性
    __Block_byref_ivar_0 *__forwarding;//栈上指向自己,如果结构体被拷贝到堆上指向堆上的拷贝的__Block_byref_ivar_0.
     int __flags;//状态标准位
     int __size;//大小
     int ivar;//原变量
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_ivar_0 *ivar; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_ivar_0 *_ivar, int flags=0) : ivar(_ivar->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_ivar_0 *ivar = __cself->ivar; // bound by ref
    
                        (ivar->__forwarding->ivar) += 1;
                        NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_76e773_mi_3,&(ivar->__forwarding->ivar));
                    }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->ivar, (void*)src->ivar, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->ivar, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
                    __attribute__((__blocks__(byref))) __Block_byref_ivar_0 ivar = {(void*)0,(__Block_byref_ivar_0 *)&ivar, 0, sizeof(__Block_byref_ivar_0), 1};
                    NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_76e773_mi_2,&(ivar.__forwarding->ivar));
                    void (*tmpBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_ivar_0 *)&ivar, 570425344));
                    (ivar.__forwarding->ivar) += 1;
                    NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_76e773_mi_4,(ivar.__forwarding->ivar));
                    ((void (*)(__block_impl *))((__block_impl *)tmpBlock)->FuncPtr)((__block_impl *)tmpBlock);
                    NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_76e773_mi_5,(ivar.__forwarding->ivar));
                    NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_76e773_mi_6,tmpBlock);
        }
        return 0;
    }
    

    会发现多了一个 __Block_byref_ivar_0 结构体,这个结构体就是__block修饰的变量要转换的结构体, __main_block_impl_0捕获的就是这个结构体指针,Block的实现函数__main_block_func_0中通过__forwarding这个成员变量指向自己或堆上的拷贝的结构体,然后再访问成员变量中的原变量ivar来修改值. ivar打印的地址不同是因为__Block_byref_ivar_0结构体被拷贝到了堆区,自然地址也不同,至于为什么会被拷贝到堆区,后面会讲.

    接下来再看一下对象类型变量加__block前缀的代码及转换的源码:

       __block NSArray *arr = @[@"1"];
            
               void (^tmpBlock)(void) = ^{
                    arr = @[@"2"];
               };
            tmpBlock();
    
    struct __Block_byref_arr_0 {
      void *__isa;
    __Block_byref_arr_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     NSArray *arr;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_arr_0 *arr; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_arr_0 *_arr, int flags=0) : arr(_arr->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_arr_0 *arr = __cself->arr; // bound by ref
    
                    (arr->__forwarding->arr) = ((NSArray *(*)(Class, SEL, ObjectType  _Nonnull const * _Nonnull, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(1U, (NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_aeef45_mi_1).arr, 1U);
               }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->arr, (void*)src->arr, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->arr, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            __attribute__((__blocks__(byref))) __Block_byref_arr_0 arr = {(void*)0,(__Block_byref_arr_0 *)&arr, 33554432, sizeof(__Block_byref_arr_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSArray *(*)(Class, SEL, ObjectType  _Nonnull const * _Nonnull, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(1U, (NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_aeef45_mi_0).arr, 1U)};
    
               void (*tmpBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_arr_0 *)&arr, 570425344));
            ((void (*)(__block_impl *))((__block_impl *)tmpBlock)->FuncPtr)((__block_impl *)tmpBlock);
        }
        return 0;
    }
    

    会发现__Block_byref_arr_0结构体多了两个函数指针,它们分别指向__Block_byref_id_object_copy_131和__Block_byref_id_object_dispose_131.再看上面的__block修饰的非对象类型和这个对象类型它们的__main_block_desc_0都有copy和dispose函数指针,它们都指向了__main_block_copy_0和__main_block_dispose_0函数.这些函数是干什么的?接下来让我们一探究竟.

    这里提一下,Block在使用的时候会分成栈块(_NSConcreteStackBlock)、堆块(_NSConcreteMallocBlock)、全局块(_NSConcreteGlobalBlock).下面的代码也会根据块的类型进行判断,这里提一下方便理解代码.具体介绍会放在第三节.

    下面的代码是Block拷贝时底层执行的代码:

    //ARC下给Block变量赋值的时候会调用
    // The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
    //
    id objc_retainBlock(id x) {
        return (id)_Block_copy(x);
    }
    void *_Block_copy(const void *arg) {
        struct Block_layout *aBlock;
    
        if (!arg) return NULL;
        
        // The following would be better done as a switch statement
        aBlock = (struct Block_layout *)arg;
        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            // latches on high
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            return aBlock;
        }
        else {
            // Its a stack block.  Make a copy.
            struct Block_layout *result =
                (struct Block_layout *)malloc(aBlock->descriptor->size);
            if (!result) return NULL;
            memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    #if __has_feature(ptrauth_calls)
            // Resign the invoke pointer as it uses address authentication.
            result->invoke = aBlock->invoke;
    #endif
            // reset refcount
            result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
            result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
            _Block_call_copy_helper(result, aBlock);
            // Set isa last so memory analysis tools see a fully-initialized object.
            result->isa = _NSConcreteMallocBlock;
            return result;
        }
    }
    static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
    {
        struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
        if (!desc) return;
    
        (*desc->copy)(result, aBlock); // do fixup
    }
    static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
    {
        if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
        uint8_t *desc = (uint8_t *)aBlock->descriptor;
        desc += sizeof(struct Block_descriptor_1);
        return (struct Block_descriptor_2 *)desc;
    }
    

    通过上面的代码及注释我们可以了解到了Block拷贝的基本流程,里面有一些枚举值,如下:

    // Values for Block_layout->flags to describe block objects
    enum {
        BLOCK_DEALLOCATING =      (0x0001),  // runtime
        BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
        BLOCK_NEEDS_FREE =        (1 << 24), // runtime
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
        BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
        BLOCK_IS_GC =             (1 << 27), // runtime
        BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
        BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
    };
    

    通过注释得知这些枚举值用于描述块对象的Block_layout-> flags的值.没错就是上面void (*tmpBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_ivar_0 *)&ivar, 570425344));中传的实参570425344.我们来打印一下按位逻辑或的值NSLog(@"%d",1 << 25 | 1 << 29); LearningDemo[43493:8342154] 570425344 刚好是传的那个值,也就是BLOCK_HAS_COPY_DISPOSE| BLOCK_USE_STRET.也就是代码会继续走(*desc->copy)(result, aBlock);方法, 然后就调用到了我们刚刚所提到的__main_block_copy_0.而__main_block_copy_0中的_Block_object_assign,它到底干了什么,下面是它源码:

    //
    // When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
    // to do the assignment.
    //
    void _Block_object_assign(void *destArg, const void *object, const int flags) {
        const void **dest = (const void **)destArg;
        switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
          case BLOCK_FIELD_IS_OBJECT:
            /*******
            id object = ...;
            [^{ object; } copy];
            ********/
    
            _Block_retain_object(object);
            *dest = object;
            break;
    
          case BLOCK_FIELD_IS_BLOCK:
            /*******
            void (^object)(void) = ...;
            [^{ object; } copy];
            ********/
    
            *dest = _Block_copy(object);
            break;
        
          case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
          case BLOCK_FIELD_IS_BYREF:
            /*******
             // copy the onstack __block container to the heap
             // Note this __weak is old GC-weak/MRC-unretained.
             // ARC-style __weak is handled by the copy helper directly.
             __block ... x;
             __weak __block ... x;
             [^{ x; } copy];
             ********/
    
            *dest = _Block_byref_copy(object);
            break;
            
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
            /*******
             // copy the actual field held in the __block container
             // Note this is MRC unretained __block only. 
             // ARC retained __block is handled by the copy helper directly.
             __block id object;
             __block void (^object)(void);
             [^{ object; } copy];
             ********/
    
            *dest = object;
            break;
    
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
            /*******
             // copy the actual field held in the __block container
             // Note this __weak is old GC-weak/MRC-unretained.
             // ARC-style __weak is handled by the copy helper directly.
             __weak __block id object;
             __weak __block void (^object)(void);
             [^{ object; } copy];
             ********/
    
            *dest = object;
            break;
    
          default:
            break;
        }
    }
    

    根据上面拷贝逻辑可知,该函数传的参数destArg是拷贝到堆上的目标Block的成员变量, object是源Block的成员变量,也就是说该函数主要是针对捕获变量的操作.最后的flags也是个枚举,如下:

    // Values for _Block_object_assign() and _Block_object_dispose() parameters
    enum {
        // see function implementation for a more complete description of these fields and combinations
        BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ... 对象类型变量
        BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable 块变量
        BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable     __Block_byref_arr_0结构体
        BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers   弱引用变量
        BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines. 
    };
    

    接下来看看这些枚举值的case中都做了什么.

    BLOCK_FIELD_IS_OBJECT中执行了_Block_retain_object,它的实现就是下面的代码,所以它其实什么都没做.然后将object的地址赋值给拷贝后的block成员变量.

    static void _Block_retain_object_default(const void *ptr __unused) { }
    static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
    

    BLOCK_FIELD_IS_BLOCK当块中捕获到了Block变量时的操作,又会去调用_Block_copy函数.

    BLOCK_FIELD_IS_BYREF也就是__block修饰变量拷贝的操作,也就是__main_block_copy_0函数传的那个8/*BLOCK_FIELD_IS_BYREF*/,然后内部又调用了下面函数

     static void __Block_byref_id_object_copy_131(void *dst, void *src) {
    _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
    }
    

    而其中的131则是128+3,官方解释如下:

    When a __block variable is either a C++ object, an Objective-C object, or another Block then the compiler also generates copy/dispose helper functions. Similarly to the Block copy helper, the "__block" copy helper (formerly and still a.k.a. "byref" copy helper) will do a C++ copy constructor (not a const one though!) and the dispose helper will do the destructor. And similarly the helpers will call into the same two support functions with the same values for objects and Blocks with the additional BLOCK_BYREF_CALLER (128) bit of information supplied.
    So the __block copy/dispose helpers will generate flag values of 3 or 7 for objects and Blocks respectively, with BLOCK_FIELD_IS_WEAK (16) or'ed as appropriate and always 128 or'd in, for the following set of possibilities:
    __block id 128+3 (0x83)
    __block (^Block) 128+7 (0x87)
    __weak __block id 128+3+16 (0x93)
    __weak __block (^Block) 128+7+16 (0x97)

    下面看看执行的_Block_byref_copy函数,源码如下:

    // Runtime entry points for maintaining the sharing knowledge of byref data blocks.
    
    // A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
    // Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
    // We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
    // Otherwise we need to copy it and update the stack forwarding pointer
    static struct Block_byref *_Block_byref_copy(const void *arg) {
        struct Block_byref *src = (struct Block_byref *)arg;
    
        if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
            // src points to stack   👇 拷贝Block_byref结构体
            struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
            copy->isa = NULL;
            // byref value 4 is logical refcount of 2: one for caller, one for stack
            copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
            copy->forwarding = copy; // patch heap copy to point to itself
            src->forwarding = copy;  // patch stack to point to heap copy
            copy->size = src->size;
     //****👆上面这段代码就是栈上的Block成员变量forwarding指向堆上的Block的过程****//
    
            if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {//👈初始化__Block_byref_arr_0的时候,传的33554432刚好是 1<<25打的值.
                // Trust copy helper to copy everything of interest
                // If more than one field shows up in a byref block this is wrong XXX
                struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
                struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
                copy2->byref_keep = src2->byref_keep;
                copy2->byref_destroy = src2->byref_destroy;
    
                if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                    struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                    struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                    copy3->layout = src3->layout;
                }
    
                (*src2->byref_keep)(copy, src);
            }
            else {
                // Bitwise copy.
                // This copy includes Block_byref_3, if any.
                memmove(copy+1, src+1, src->size - sizeof(*src));
            }
        }
        // already copied to heap
        else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
            latching_incr_int(&src->forwarding->flags);
        }
        
        return src->forwarding;
    }
    

    上面的代码执行顺序就是 块拷贝->__main_block_copy_0 ->__Block_byref_id_object_copy_131

    剩下的case都是指向源变量地址.

    到此为止你应该已经发现一开始的例子__main_block_desc_0中没有copy和dispose函数指针的原因就是它捕获的是非对象类型,而有这两个函数指针的是对象类型和__block修饰的变量,用于拷贝捕获的变量.另外,__block修饰的非对象类型是没有__Block_byref_id_object_copy_131和__Block_byref_id_object_dispose_131的,因为__Block_byref作为对象是需要这些拷贝操作的,而里面的非对象类型变量不需要.
    另外关于_Block_object_dispose的底层实现我就不在赘述了,和copy过程类似,看一下源码就懂了.

    三、关于栈块、堆块、全局块

    根据Block所在内存区域不同分为栈块(_NSConcreteStackBlock)、堆块(_NSConcreteMallocBlock)、全局块(_NSConcreteGlobalBlock).它们在ARC和MRC下的表现是有差异的.

    • MRC 下的表现
      堆块:为Block执行copy方法才会变成堆块.赋值到有copy、strong修饰符的属性变量也是堆块.
      栈块:写在方法里的默认就是栈块.
      全局块:全局变量区域定义的块.不赋值且捕获了自动变量的也是全局块.
    • ARC下的表现
      堆块:块实现赋值到块自动变量上就会变成堆块.因为默认有__strong修饰符的缘故,被持有.
      栈块:不赋值的块且捕获了自动变量的默认是栈块.
      全局块:全局变量区域写的块.块里面使用的是全局变量、全局静态变量、静态变量、或者不使用任何变量的时候也是全局块.

    了解当前的Block是那种类型的块以及如何会变成这种类型的块之后,再结合上面的关于Block捕获变量的底层实现和拷贝过程我们就清楚的明白当前的块能不能修改变量,变量有没有执行拷贝了.

    四、常见实战应用

    1、适配器模式

    用Block作为适配器来传值,可以降低代码分散程度,在数据种类不是很多的时候可以代替委托协议来使用.被适配者在Block的实现中执行操作.相对协议更加轻松的实现了适配器模式.

    2、循环引用问题

    一般我们会用__weak 来修饰self变量来打破循环引用,因为__weak修饰的变量Block不会持有它,执行拷贝操作引用计数也不会增加,但是在Block的实现内记得用__strong再修饰一边self变量,防止外面的变量提前释放或者被置空导致访问错误.原理也很简单,因为Block的结构体会捕获self,加上__weak修饰符就可以不持有self变量来,也就不会造成循环引用.而__strong是加在Block的实现里的,也不会造成循环引用,又可以保证代码的安全访问.

    相关文章

      网友评论

        本文标题:iOS Block从入门到完全详解

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