美文网首页
iOS开发——block底层实现与变量捕获

iOS开发——block底层实现与变量捕获

作者: 原来是泽镜啊 | 来源:发表于2020-08-04 22:18 被阅读0次

    前言

    首发地址:block底层实现与变量捕获

    带着问题阅读

    1. block的本质是什么?你能讲出来它的底层结构吗?
    2. 全局变量会被block捕获吗?block会捕获哪些变量?

    block的底层数据结构

    block又叫代码块,是OC语法中非常重要的一个概念,我们先来看一下Block的简单使用。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            ^{
                NSLog(@"hello block");
            }();
    
            int d = 5;
            void (^block)(int, int) = ^(int a, int b) {
                int c = a + b + d;
                NSLog(@"a + b + d = %d", c);
            };
            block(3, 4);
        }
        return 0;
    }
    复制代码
    

    上面的代码中,我们创建了两个Block,一个直接执行,输出Hello World。 一个通过block变量进行调用,并引用了一个外部变量d。输出12

    作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:413038000,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

    推荐阅读

    iOS开发——最新 BAT面试题合集(持续更新中)

    我们将以上代码编译成C代码:

    # 在main.m所在目录执行该命令。
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
    复制代码
    

    从main-arm64.cpp文件中,我们可以看到Block的结构如下:

    struct __main_block_impl_1 {
      struct __block_impl impl;
      struct __main_block_desc_1* Desc;
      int d;
      __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _d, int flags=0) : d(_d) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    复制代码
    

    我们可以看出Block的底层是结构体,__main_block_impl_1 包含一个变量impl 其结构和 Class的结构类似,其包含一个isa指针,可见Block本质上也是一个类,其中FuncPtr表示要执行的代码块的函数地址。d表示它引用的外部变量。

    下面,我们一起看一下Block的调用过程,首先我们将下面代码,编译成C代码。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^block)(void) =  ^{
                NSLog(@"hello block");
            };
            block();
        }
        return 0;
    }
    
    // 下面是编译后的C代码
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
            //可以看得出来,Block的调用集中在这两行。
            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;
    }
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_bv_k_7y193n6tvf34wjvqvnn3q40000gn_T_main_0422f2_mi_0);
            }
    
    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)};
    复制代码
    

    针对,上面的两行代码,先调用__main_block_impl_0的结构体构造函数,创建Block,并将地址赋值给我block。而__main_block_func_0 是对应block内部要执行的代码,是一个静态的方法,它会赋值给__block_impl中的FuncPtr

    __main_block_desc_0_DATA是也是一个结构体变量,里面的两个参数reserved 为 0, Block_size__main_block_impl_0 结构体的大小。

    调用的时候,是从block里面直接取出FuncPtr。 我们知道block__main_block_impl_0类型,由于结构体的特性,将block强转为__block_impl类型,是可以直接取到FuncPtr的。所以第二句的调用也是清晰的。

    上面的两句代码去掉强制类型转化,可以精简为:

    void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
    block->FuncPtr(block);
    复制代码
    

    这样,调用过程就清晰多了。

    通过上面的分析,我们可以看出block的结构应该是如下图所示:

    [图片上传中...(image-107b00-1595249453719-0)]

    变量捕获

    auto自动变量

    auto自动变量是离开作用域,就会销毁, 只存在局部变量里面,不能修饰全局变量。

    比如,下面例子中的ageweight 就是auto变量,他们离开自己所在的作用局就会销毁。默认情况下auto关键字会自动添加。

    int main(int argc, const char * argv[]) {
        @autoreleasepool { 
          {
            int age = 20;
            auto int weight = 60;
          }
          // 在这里访问age, weight就报错了。
        }
        return 0;
    }
    复制代码
    

    如果block中使用了auto变量,那么block就会捕获该变量,下面代码

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            {
              int age = 20;
              auto int weight = 60;
    
                void (^block)(void) =  ^{
                    NSLog(@"age = %d, weight = %d", age, weight); //age的结果是20
                };
                age = 40;
                block();
            }
        }
        return 0;
    }
    复制代码
    

    打印的结果中 age为20 还是 40? 编译后,__main_block_impl_0的结构如下,增加了两个int 变量。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int age;
      int weight;
      //: age(_age), weight(_weight) 是C++语法,表示参数_age会赋值给变量age.
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int _weight, int flags=0) : age(_age), weight(_weight) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    复制代码
    

    我们可以看出,捕获了auto变量,而且是值传递。

    static变量

    下面代码输入结果是什么?

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            {
                static int height = 40;
                void (^block)(void) =  ^{
                    NSLog(@"height = %d, ", height);
                };
                height = 80;
                block();
            }
        }
        return 0;
    }
    复制代码
    

    结果是80,为什么呢? 我们依然通过编译后的结果查看。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *height;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_height, int flags=0) : height(_height) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            {
                static int height = 40;
                void (*block)(void) = ((void (*)())&__ma.in_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &height));
                height = 80;
                ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
            }
        }
        return 0;
    }
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *height = __cself->height; // bound by copy
    
                    NSLog((NSString *)&__NSConstantStringImpl__var_folders_bv_k_7y193n6tvf34wjvqvnn3q40000gn_T_main_8d6bbf_mi_0, (*height));
                }
    复制代码
    

    我们可以看出__main_block_impl_0中增加了一个变量height,但需要注意的是它是int * 类型的,在给它赋值的时候传入的是&height 。 在__main_block_func_0中访问的时候是通过*height取值的。

    因此我们可以得出结论,静态变量也是会被Block捕获的,但它捕获的是指针。

    全局变量

    下面代码,输出的结果是什么?

    int age = 10;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            {
                void (^block)(void) =  ^{
                    NSLog(@"height = %d, ", age);
                };
                age = 20;
                block();
            }
        }
        return 0;
    }
    复制代码
    

    输入结果是20,那block捕获了age吗?是通过指针访问的吗?我们看一下编译结果:

    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;
      }
    };
    复制代码
    

    可以看出block并没有捕获全局变量。

    结论

    通过上面的分析,我们可以得出结论:

    • 对于全局变量,block不会捕获,通过全局变量访问。
    • 对于局部变量,auto自动变量将会捕获,且是值传递。
    • 对于局部变量,static变量将会捕获,且是指针传递。

    捕获self

    下面代码中,Person类中的test方法中block捕获了变量了吗?捕获了那个变量?

    // main.m
    #import <Foundation/Foundation.h>
    #import "Person.h"
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *p = [[Person alloc] init];
            p.name = @"乐戈";
            [p test];
        }
        return 0;
    }
    
    // Person.h
    @interface Person : NSObject
    @property (nonatomic, copy)NSString *name;
    - (void)test;
    @end
    
    // Person.m
    @implementation Person
    - (void)test {
        void (^block)(void) = ^{
            NSLog(@"name == %@", self.name);
        };
        block();
    }
    @end
    复制代码
    

    用上面的命令将Person.m编译C++代码,如下:

    struct __Person__test_block_impl_0 {
      struct __block_impl impl;
      struct __Person__test_block_desc_0* Desc;
      Person *self;
      __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    复制代码
    

    可以看出,这里捕获的并不是name,而是Person对象,这涉及了block的循环引用,我们将在下面的文章中讲述。

    思考题

    下面各个代码的输出结果是什么?

    //问题1
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSMutableArray *array = [@[@"abc"] mutableCopy];
            void (^block)(void) =  ^{
              NSLog(@"hello block---%@", [array firstObject]);
            };
            array[0] = @"dgf";
            block();
        }
        return 0;
    }
    
    //问题2
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSMutableArray *array = [@[@"abc"] mutableCopy];
            void (^block)(void) =  ^{
              NSLog(@"hello block---%@", [array firstObject]);
            };
            array =  [@[@"dgf"] mutableCopy];
            block();
        }
        return 0;
    }
    复制代码
    

    交流互动

    作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:413038000,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

    推荐阅读

    iOS开发——最新 BAT面试题合集(持续更新中)

    相关文章

      网友评论

          本文标题:iOS开发——block底层实现与变量捕获

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