面试遇到block的第一天-本质和变量捕获

416 阅读6分钟

block的本质

  • block本质上也是一个OC对象,他的内部有一个isa指针
  • block是封装了函数调用以及函数调用环境(比如函数参数)的OC 对象
  • block也是一个匿名函数

写一个简单的block demo验证分析一下block的本质:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        void(^myBlock)(void) = ^{
            
            NSLog(@"this is a block");
        };
        myBlock();
        NSLog(@"%@",myBlock);
        
    }
    return 0;
}

使用clang命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m编译成C++代码,找到C++代码最后我们可以看到block被编译成一个结构体

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_impl又是一个结构体

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

这里isa指针是指向了block的类型,这个后面再说,在后面贴的C++代码中可以注意一下,都是block的类型。
我们这个简单的myBlock内部只有一句NSLog的打印,被封装在了__main_block_func_0这个函数中

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ljphgvys0hzg8hfxdtv5q5gm0000gn_T_main_26cca1_mi_0);
        }

__main_block_func_0的函数地址又作为__main_block_impl_0构造函数的第一个参数fp,被赋值给implFuncPtr

最后通过((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);调用FuncPtr,也就是调用myBlock中封装的函数,并且把block本身作为参数传进去,这里传本身进去也是有说法的,后面就会用到,接着往下看。

根据以上对源码的分析 ,也验证了我们最开始说的两点block的本质。

变量捕获

什么是变量捕获,block为什么要捕获外部变量,其实这个现象很常见,只是这么描述出来感觉有点儿抽象,比如在上面demo中,在block内部添加使用self,或者self.property,那么block中使用到的值,就会被捕获到block内部,那这些变量是以什么样的形式又是如何被block捕获到的呢。下面我们就由浅到深的继续探究block是如何获取外部变量的。

局部变量(auto变量)

所谓auto变量也叫自动变量,就是离开作用域(他所在的{})就会自动销毁,也就是局部变量,因为auto可以省略,所以我们平时都可以直接定义局部变量,不需要在前面加上auto修饰符。
先看一段示例代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void(^myBlock)(void) = ^{
            NSLog(@"this is a block - %d ",age);
        };
        age = 20;
        myBlock();        
    }
    return 0;
}

那么请问这段代码的打印age的值是多少呢?有人觉得是20,有人觉得是10,这里肯定的告诉大家,或者大家手动写一写这段代码测试一下打印结果,其实打印结果是10.还是用clang编译成C++代码一看究竟

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ljphgvys0hzg8hfxdtv5q5gm0000gn_T_main_edbb92_mi_0,age);
        }

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 age = 10;
        void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        age = 20;
        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    }
    return 0;
}

同样的结构体,同样的调用方式,上面说过的内容就不重复了,主要看auto变量age是如何被block获取到的:

  1. __main_block_impl_0结构体中添加了一个age的成员变量,构造函数中也添加了age: age(_age) 是C++语法,表示默认执行age = _age
  2. void(*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); 直接把age的值获取到,并且保存在结构体内部,因为是值传递,并没有捕获到age变量的内存地址
  3. __main_block_func_0中通过参数__cself,也就是上面提过的把block自身作为入参,然后通过__cself取出age
  4. age = 20; 虽然修改了age的值,但是block捕获的age的值并不会发生改变

基于这一点,在编译时,就抛出了auto变量无法在block内部被修改的错误

static变量

auto相对的,还有static修饰的变量,那么blockstatic变量捕获的方式和auto变量有什么不同呢?看下面的测试代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int age = 10;
        static int height = 50;
        void(^myBlock)(void) = ^{
            NSLog(@"age is - %d ,height is %d",age, height);
        };
        age = 20;
        height = 100;
        myBlock();        
    }
    return 0;
}

输出

block[87871:3465064] age is - 10 ,height is 100

很明显我们在block定义之后修改static变量的值,block内部是可以知道的,再看C++代码中两个变量被捕获到结构体中的区别

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *height;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

根据上面编译完的代码可以很明显看到变量age和height被block获取时的区别,height被结构体__main_block_impl_0存储的是内存地址,所以block中可以修改height的值。

全局变量

继续根据测试代码和C++代码,分析block对变量的捕获

#import <Foundation/Foundation.h>
int age = 10;
static int height = 50;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^myBlock)(void) = ^{
            NSLog(@"this is a block - %d ,height is %d",age, height);
        };
        age = 20;
        height = 100;
        myBlock();        
    }
    return 0;
}

C++代码:

#pragma clang assume_nonnull end
int age = 10;
static int height = 50;

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的调用,是在跨函数访问变量,如果不捕获到block内部,访问auto变量的时候已经释放了

self的捕获

其实self的捕获,才是开发中经常遇到的情况,也是引起block循环引用的罪魁祸首。当然self的捕获可以归到上面两类中,但还是想拿出来单独分析一下。也算是对上面两类变量分析结果的实践吧。

#import "Person.h"

@implementation Person
- (void)test_block {
    void(^block)(void) = ^{
        NSLog(@"this is a block - %p",self);
    };
}
@end

先思考一下,这里self会被捕获到block中嘛,self在这里是一个auto变量还是static变量还是全局变量呢?先分析出是什么变量,下一步我们才能知道self是以什么形式被捕获到block中或者会不会被捕获到block中。

你知道答案了吗?

看看下面的C++代码:

struct __Person__test_block_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_block_desc_0* Desc;
  Person *self;
  __Person__test_block_block_impl_0(void *fp, struct __Person__test_block_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以得到结论,block是会捕获self的,可是self是个局部变量嘛?这里又要提到method的两个默认参数了self_cmd,代码中的test方法被编译成_I_Person_test_block,他有两个参数是self_cmd,函数的参数就是局部变量,然后被捕获到block中赋值给struct中的self

static void _I_Person_test_block(Person * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__Person__test_block_block_impl_0((void *)__Person__test_block_block_func_0, &__Person__test_block_block_desc_0_DATA, self, 570425344));
}

同理,如果是block中访问self的成员变量(self.name)或者通过self调用别的方法([self method]),也同样会捕获self到block中。