程序员社区

OC-Block的本质(一)-底层结构、变量捕获

OC-Block的本质(一)-底层结构、变量捕获插图
image-20210422152708323

本篇学习内容

OC-Block的本质(一)-底层结构、变量捕获插图1
image-20210422152804224
OC-Block的本质(一)-底层结构、变量捕获插图2
image-20210422152831028
OC-Block的本质(一)-底层结构、变量捕获插图3
image-20210422152856465

block的本质

block的原理是怎样的?本质是什么?

  • block本质上也是一个OC对象,因为它的内部也有个isa指针
  • block是封装了函数调用以及函数调用环境的OC对象

通俗的理解:block就是将一些代码封装起来,以便在将来某个时候被使用,如果你不去调用block,block内部封装的代码就不会执行。

举一个简单的例子

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ^{
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
        };
    }
    return 0;
}

RUN>

没有任何输出,Block代码块没调用

Block的使用也很简单,可以像函数一样被使用。加上()就代表调用,如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ^{
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
        }();
    }
    return 0;
}

RUN>

2021-04-22 15:35:54.161606+0800 Interview03-block[4322:148955] this is a block!
2021-04-22 15:35:54.162084+0800 Interview03-block[4322:148955] this is a block!
2021-04-22 15:35:54.162138+0800 Interview03-block[4322:148955] this is a block!
2021-04-22 15:35:54.162160+0800 Interview03-block[4322:148955] this is a block!

block的底层结构-block的本质探索

写个简单的block,其中block内部使用了block外部的age变量:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        int age = 20;

        void (^block)(int, int) =  ^(int a , int b){
            NSLog(@"this is a block! -- %d", age);
        };
        block(10, 10);
    }
    return 0;
}

RUN>

2021-04-22 15:38:10.449976+0800 Interview03-block[4340:150550] this is a block! -- 20

上面的代码可以看出,block里面使用了它上面的 int age = 20 ,可以将这个先简单的理解成函数调用环境,顾名思义,就是block所用到的一些外部变量。

通过clang编译器执行编译成C++代码:

$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

clang编译器编译完后会得到一个.cpp格式的文件,这就是我们刚才转换的.m文件的底层代码.

main函数的C++代码如下

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 20;
        //block底层定义
        void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        //block底层调用
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
    }
    return 0;
}

但是由于底层的代码添加了许多强转,我们简化代码,如下:

//block底层定义
void (*block)(void) = &__main_block_impl_0(
                                           __main_block_func_0,
                                           &__main_block_desc_0_DATA
                                           );
//block底层调用
block->FuncPtr(block, 10, 10);
block底层定义
//block底层定义
void (*block)(void) = &__main_block_impl_0(
                                           __main_block_func_0,
                                           &__main_block_desc_0_DATA
                                           );

先看__main_block_impl_0这个函数,我们发现它被定义在一个同名结构体里面,这个__main_block_impl_0结构体就是block的底层实现

// 一: block底层数据结构
struct __main_block_impl_0 {
    struct __block_impl impl; // 1: impl 结构体
    struct __main_block_desc_0* Desc; // 2: block描述信息的结构体
    int age; //3:捕获的外部变量
    //4: 和结构体同名的构造函数 ( C++语法 , 类似于 OC 的init方法,返回一个结构体对象,类似于返回self)
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age){
    impl.isa = &_NSConcreteStackBlock;//isa指向类对象,就比如str的isa指向NSString
    impl.Flags = flags;
    impl.FuncPtr = fp;//外面的__main_block_func_0函数地址传进来,保存在这里
    Desc = desc;//外面的__main_block_desc_0结构体地址传进来,保存在这里
  }
};

通过底层代码我们可以看到,block在底层中的数据结构是一个结构体,这个结构体有四个部分组成:

1: struct __block_impl

2: struct __main_block_desc_0

3: 捕获的外部变量

4:和block结构体同名的构造函数

我们找到struct __block_impl 结构体:

//struct __block_impl 结构体
struct __block_impl {
  void *isa; //指向 block 的类型
  int Flags;//按位表示block的附加信息
  int Reserved;//保留变量
  void *FuncPtr; //封装了执行 block 代码块的函数地址
};

发现这个结构体里面第一个成员就是isa,验证了block本质上也是一个OC对象。

然后我们再找到struct __main_block_desc_0 结构体 :

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)};

可以发现,这个结构体被重新命名为__main_block_desc_0_DATA,默认传入了两个值0和sizeof(struct __main_block_impl_0),block的底层就是__main_block_impl_0结构体,所以这个结构体==第二个值保存的是block的大小==

接下来我们看一下__main_block_impl_0函数的参数,第一个参数是指向__main_block_func_0函数的指针,如下:

//封装了block执行逻辑的函数
//第一个参数是block,后面是block调用的时候传入的参数
void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int age = __cself->age; // bound by copy

  NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_0, age);
}

现在我们知道了,首先__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age)函数有两个参数,第一个参数是__main_block_func_0函数的地址(这个函数里面封装了我们block里面执行的代码),第二个参数是__main_block_desc_0结构体的地址(这个结构体里面有保存block的大小),构造函数的返回值是个__main_block_impl_0结构体,block底层就是__main_block_impl_0结构体,最后再获取__main_block_impl_0结构体的地址,赋值给左边的“block”变量,然后我们拿到“block”变量就可以做其他事情了,至此,block定义完成。

block底层调用
//block底层调用 
block->FuncPtr(block, 10, 10);

这句代码就很简单了,直接取出block里面的FuncPtr函数,传入参数进行调用。

不应该是通过“block-> impl->FuncPtr(block, 10, 10)”来拿到FuncPtr吗?其实我们在简化之前,代码是这样的:

((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);

可以发现系统把block强转成__block_impl类型的了,由于impl又是__main_block_impl_0结构体的第一个成员,所以impl的地址和__main_block_impl_0结构体的地址是一样的,强转之后可以直接获取到FuncPtr。

根据如上分析,验证了,block是封装了函数调用以及函数调用环境的OC对象

__main_block_impl_0``__block_impl__main_block_desc_03个结构体之间的关系

OC-Block的本质(一)-底层结构、变量捕获插图4
block的本质

咋们通过梳理了block底层几个类的关系,现在调整一下代码

struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};

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 age;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        
        int age = 20;

        void (^block)(int, int) =  ^(int a , int b){
            NSLog(@"this is a block! -- %d", age);
        };



        struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;



        block(10, 10);
    }
    return 0;
}

加断点,验证一下内存

OC-Block的本质(一)-底层结构、变量捕获插图5
image-20210422161112236

FuncPtr地址验证

OC-Block的本质(一)-底层结构、变量捕获插图6
image-20210422161234946

总结:

如上图所示,block底层就是一个__main_block_impl_0结构体,它由三个部分组成:

  1. 第一部分是impl,它是个结构体,里面有isa指针和FuncPtr指针,FuncPtr指针指向__main_block_func_0函数,这个函数里面封装了block需要执行的代码。
  2. 第二部分是desc,它是个指针,指向__main_block_desc_0结构体,它里面有一个Block_size用来保存block的大小。
  3. 第三部分是age,它把外面访问的成员变量age封装到自己里面了。

block的变量捕获(capture)

OC-Block的本质(一)-底层结构、变量捕获插图7
image-20210422161702137
  1. 如果是被auto修饰的局部变量,会被捕获,是值传递
  2. 如果是被static修饰的局部变量,会被捕获,是指针传递
  3. 如果是全局变量,不会被捕获,因为可以直接访问
一:auto变量
  • auto变量:自动变量,离开作用域就会销毁,一般我们创建的局部变量都是auto变量,比如 int age = 10,系统会在默认在前面加上auto int age = 10
    首先我们要搞清楚,什么是捕获,所谓捕获外部变量,意思就是在block内部,创建一个变量来存放外部变量,这就叫做捕获.
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       int age = 10;
        void (^block)(void) = ^{
            NSLog(@"age is %d",age);
        };
        age = 20;
        block();
    }
    return 0;
}
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
{
        int age = 10;
       //定义block
        void (*block)(void) = &__main_block_impl_0(
                                                   __main_block_func_0,
                                                   &__main_block_desc_0_DATA,
                                                   age
                                                   );
        age = 20;
       //调用block
        block->FuncPtr(block);
    return 0;
}

我们看到在调用block的构造函数时,传入了三个参数,分别是:__main_block_func_0,&__main_block_desc_0_DATA,age,我们找到block的构造函数,看看内部如何处理这个age:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;  // 1: 定义了一个同名的age变量
//block构造函数
  __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;
    age = _age; //2 :C++的特殊语法,在构造函数内部会默认把_age赋值给age
  }
};

通过查看block的内部结构看我们发现,block内部创建了一个age变量,并且在block构造函数中,把传递进来的_age赋值给了这个age变量.我们看看调用block时,他的底部取的是哪个age:

//调用block的FuncPtr函数,把block当做参数传递进去
block->FuncPtr(block);
//FuncPtr函数内部
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//通过传递的block,找到block内部的age
  int age = __cself->age; // bound by copy
//打印age
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_3089d7_mi_0,age);
        }

通过底层代码,我们看到,在调用block时,block会找到自己内部的age变量,然后打印数出,所以我们修改age = 20,并不会影响block内部的age

二:static变量

我们执行如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       auto int age = 10; 
       static int height = 10;

    void (^block)(void) = ^{
       // age的值捕获进来
       // height的指针捕获进来
       NSLog(@"age is %d, height is %d", age, height);
    };

      age = 20;
      height = 20;

      block();

    }
    return 0;
}

RUN>

2021-04-22 16:31:33.658732+0800 Interview01-Block的本质[4670:177646] age is 10, height is 20
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

代码转成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;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *height = __cself->height; // bound by copy

  NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_12eb7d_mi_1, age, (*height));
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    auto int age = 10;
    static int height = 10;

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));//&height传递指针

    age = 20;
    height = 20;

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }

    return 0;
}

可以看出age和height都被捕获了,age是值捕获,height是指针捕获。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;//定义 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,不同的是,height是一个指针指针变量,用于存放外部变量的指针.我们再来看看执行block代码块的内部:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *height = __cself->height; // bound by copy
 // *height : 取出指针变量所指向的内存的值
   NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_bf6cae_mi_0,age,(*height));
        }

我们看到,对于age是捕获到内部,把外部age的值存起来,而对于height,是把外部变量的指针保存起来,所以,我们在修改height时,会影响到block内部的值

  1. 我们定义block,其实就是初始化__main_block_impl_0结构体,定义block的时候会把age和&height传进去,这个结构体里面有一个age一个height指针用于接收传进去的值,这行代码“age(_age), height(_height)”就是保证外面的变量改变的时候实时改变结构体里面的age和height指针的值。
  2. 我们调用block的时候,其实就是执行__main_block_func_0函数,这个函数会获取__main_block_impl_0结构体中age和height指针的值,所以打印的时候就会把age和*height的值打印出来。
  3. 所以执行完block之后age的值没改变,因为是值传递,height的值改变了,因为是指针传递。

为什么auto变量是值传递,static变量是指针传递呢?

因为auto是自动变量,出了作用域后会自动销毁的,如果我们保留他的指针,就会存在访问野指针的情况

//定义block类型
void(^block)(void);

void test(){
    int age = 10;
    static int height = 20;
//在block内部访问 age , height
    block = ^{
        NSLog(@"age is %d, height is %d",age,height);
    };
    age = 20;
    height = 20;
}

//在main函数中调用
int main(int argc, const char * argv[]) {
        test();
 //test调用后,age变量就会自动销毁,如果block内部是保留age变量的指针,那么我们在调用block()时,就出现访问野指针
        block();
}
三:全局变量

全局变量哪里都可以访问,所以block内部是不会捕获全局变量的,直接访问

int age_ = 10;
static int height_ = 10;

void (^block)(void);

void test()
{


    block = ^{
        // age的值捕获进来(capture)
        NSLog(@"age is %d, height is %d", age, height);
    };

    age = 20;
    height = 20;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        test();
        block();
    }
    return 0;
}

转成C++文件之后,代码如下:

int age_ = 10;
static int height_ = 10;

void (*block)(void);

struct __test_block_impl_0 {
  struct __block_impl impl;
  struct __test_block_desc_0* Desc;
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __test_block_func_0(struct __test_block_impl_0 *__cself) {


        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yh_qjzhl57s63j2m9l4frv27zjc0000gn_T_main_478a1b_mi_0, age, height);
    }

static struct __test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};

void test()
{
    block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA));
    age = 20;
    height = 20;
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        test();
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
OC-Block的本质(一)-底层结构、变量捕获插图8
image-20210422165620199

为什么全局变量不需要捕获?

因为全局变量无论哪个函数都可以访问,block内部当然也可以正常访问,所以根本无需捕获

为什么局部变量就需要捕获呢?

因为作用域的问题,我们在一个函数中定义变量,在block内部访问,本质上跨函数访问,所以需要捕获起来.

四:self的捕获
#import <Foundation/Foundation.h>

@interface MJPerson : NSObject

@property (copy, nonatomic) NSString *name;

- (void)test;

- (instancetype)initWithName:(NSString *)name;

@end
    
#import "MJPerson.h"

@implementation MJPerson

- (void)test
{
    void (^block)(void) = ^{
        NSLog(@"-------%d", [self name]);
    };
    block();
}

- (instancetype)initWithName:(NSString *)name
{
    if (self = [super init]) {
        self.name = name;
    }
    return self;
}

@end

MJPerson.m转成C++代码:

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

static void __MJPerson__test_block_func_0(struct __MJPerson__test_block_impl_0 *__cself) {
  MJPerson *self = __cself->self; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_MJPerson_1027e6_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
    }

static void _I_MJPerson_test(MJPerson * self, SEL _cmd) {
    void (*block)(void) = ((void (*)())&__MJPerson__test_block_impl_0((void *)__MJPerson__test_block_func_0, &__MJPerson__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

static instancetype _I_MJPerson_initWithName_(MJPerson * self, SEL _cmd, NSString *name) {
    if (self = ((MJPerson *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJPerson"))}, sel_registerName("init"))) {
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)name);
    }
    return self;
}
OC-Block的本质(一)-底层结构、变量捕获插图9
image-20210422171347998

可以看出self被捕获了,并且是指针捕获,既然被捕获,就说明self是局部变量

为什么self是局部变量呢?
其实每个方法都两个隐式参数,一个是self一个是_cmd,self是方法调用者,_cmd是方法名,既然self被当做参数了,那self肯定是局部变量了,也可以在上面的代码中进行验证,我们看一下转换后的test()方法:

static void _I_Person_test(Person * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

OC 中的test()方法时没有参数的,但是转换成 C++ 后就多了两个参数self,_cmd,其实我们每个 OC 方法都会默认有这两个参数,这也是为什么我们在每个方法中都能访问self_cmd,而参数就是局部变量,所以block就自然而然的捕获了self.

对于[self name],在上面的代码可以看出是给self发送消息,如下:

objc_msgSend((id)self, sel_registerName("name"))

所以,block会捕获self,如果想要访问self中的成员变量就给self发送消息就好了(self都被捕获了,肯定可以获取到self中的其他信息了)。

总结:

一:只要是局部变量,不管是auto 变量,还是static 变量,block都会捕获.不同的是,对于auto 变量,block是保存值,而static 变量 是保存的指针.
二:如果是全局变量,根本不需要捕获,直接访问。

特别备注

本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

赞(0) 打赏
未经允许不得转载:IDEA激活码 » OC-Block的本质(一)-底层结构、变量捕获

一个分享Java & Python知识的社区