block底层实现与变量捕获

前言

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

本文已经添加到专辑:《完全弄懂OC》。 欢迎加入个人QQ群:661461410,一块儿探讨iOS底层原理。shell

带着问题阅读

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

block的底层数据结构

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

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数据结构

咱们将以上代码编译成C代码:iphone

# 在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代码。atom

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中的FuncPtrspa

__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的结构应该是以下图所示:

block内存布局

变量捕获

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

交流互动

关注公众号