美文网首页
iOS-block1-底层结构、变量捕获、类型

iOS-block1-底层结构、变量捕获、类型

作者: Imkata | 来源:发表于2019-11-28 21:26 被阅读0次

    如下代码就是个block,block不会主动调用

    ^{
        NSLog(@"this is a block!");
    };
    

    运行之后,发现并没有打印。
    如果在block后面加个(),发现block就立马调用了:

    ^{
        NSLog(@"this is a block!");
    }();
    
    运行后:
    this is a block!
    

    一般我们都把block保存起来,在需要的时候才调用。

    一. block的底层结构(block的本质)

    block是封装了函数调用以及函数调用环境的OC对象

    下面我们验证上面这句话。
    写个简单的block,其中block内部使用了block外部的age变量:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            int age = 20;
            //block定义
            void (^block)(int, int) =  ^(int a , int b){
                NSLog(@"this is a block! -- %d", age);
            };
            age = 30;
            //block调用
            block(10, 10);
        }
        return 0;
    }
    

    block调用之后打印:this is a block! -- 20 。为什么不是30?

    源码分析

    通过“xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件”指令,将上面代码转成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的底层实现

    struct __main_block_impl_0 {
      struct __block_impl impl; 
      struct __main_block_desc_0* Desc; 
      int age;
      // 构造函数(类似于OC的init方法),返回结构体对象
      __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结构体地址传进来,保存在这里
      }
    };
    

    先看结构体里面的__main_block_impl_0函数,其实这个函数在C++中是构造方法,类似于OC的init方法,返回结构体对象。函数的第一个参数是fp指针,第二个参数是desc指针。

    接下来再看看__main_block_impl_0结构体,它第一个成员是个结构体:

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

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

    第二个成员是指向__main_block_desc_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_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);
    }
    

    可以发现,我们写的代码块里面的NSLog被封装成了__main_block_func_0函数,它引用了block外面的成员变量age。

    第二个参数就是上面我们说的__main_block_desc_0结构体的地址。

    现在我们知道了,首先__main_block_impl_0函数有两个参数,第一个参数是__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对象

    总结:

    block的本质.png

    如上图所示,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的本质,网上还有一张图,可自己参考:

    block的本质.png

    二. block的变量捕获(capture)

    在上面我们留了一个问题,block调用之后打印:this is a block! -- 20 。为什么不是30?

    局部变量和全局变量的捕获

    捕获机制.png
    1. 如果是被auto修饰的局部变量,会被捕获,是值传递
    2. 如果是被static修饰的局部变量,会被捕获,是指针传递
    3. 如果是全局变量,不会被捕获,因为可以直接访问

    auto自动变量,离开作用域就销毁,默认省略auto。比如我们常见的 int age = 10,其实就是默认省略了auto,本来应该是auto int age = 10

    我们执行如下代码:

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

    打印:

    age is 10, height is 20
    

    这就解释了上面的疑问,因为age是值捕获,所以修改外面的age值不会影响block里面的age值。

    ① 源码分析

    为了探究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;
      }
    };
    
    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));
    
        age = 20;
        height = 20;
    
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
    
        return 0;
    }
    

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

    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的时候就不可能再去访问auto变量的内存了。
    但是static变量不一样,被static修饰的变量一直在内存中,只要捕获它的指针就可以随时访问它的内存了。

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

    下面我们验证下,如下代码:

    int age_ = 10;
    static int height_ = 10;
    
    void (^block)(void);
    
    void test()
    {
        auto int a = 10;
        static int b = 10;
        block = ^{
            NSLog(@"age is %d, height is %d", a, b);
        };
    }
    
    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;
      int a;
      int *b;
      __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
      int *b = __cself->b; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_fd2a14_mi_0, a, (*b));
        }
    
    void test()
    {
        auto int a = 10;
        static int b = 10;
        block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a, &b));
    }
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            test();
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    

    可以看出age_和height_没被捕获,a和b被捕获了,相信不用解释上面的代码也能看懂。

    总结

    为什么局部变量需要捕获,全局变量不需要捕获呢?
    因为作用域的问题。局部变量的作用域在{}中,当在其他函数中想要访问局部变量的值,肯定要把它捕获啊(auto变量是值捕获,static变量是指针捕获)。对于全局变量,任何地方都可以访问,所以没必要捕获。

    ④ self的捕获

    下面代码,创建MJPerson对象:
    MJPerson.h

    #import <Foundation/Foundation.h>
    
    @interface MJPerson : NSObject
    
    @property (copy, nonatomic) NSString *name;
    
    - (void)test;
    
    - (instancetype)initWithName:(NSString *)name;
    
    @end
    

    MJPerson.m

    #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
    

    如上代码,在test方法里面访问name属性,那么self会被捕获吗?name会被捕获吗?

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

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

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

    void _I_MJPerson_test(MJPerson * self, SEL _cmd)
    

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

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

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

    总结:局部变量会捕获,全局变量不会捕获。

    Demo地址:block的本质和变量捕获

    三. block的类型

    接下来我们讲的都是在MRC环境下。

    block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承于NSBlock类型

    __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
    __NSMallocBlock__ ( _NSConcreteMallocBlock )
    __NSStackBlock__ ( _NSConcreteStackBlock )

    1. 验证block是OC对象

    前面我们说了,block是OC对象,既然是OC对象就可以调用class方法查看类型

    下面验证block是OC对象,运行代码:

    void (^block)(void) = ^{
                NSLog(@"Hello");
            };
            
    NSLog(@"%@", [block class]);
    NSLog(@"%@", [[block class] superclass]);
    NSLog(@"%@", [[[block class] superclass] superclass]);
    NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
    

    打印结果:

    __NSGlobalBlock__
    __NSGlobalBlock
    NSBlock
    NSObject
    

    可以看出上面的block继承关系是:__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
    上面的block最终继承于NSObject,说明block是OC对象。现在我们也明白了,block内部的isa就是从NSObject中获取的,而且isa指向block的类对象。

    2. 三种block在内存中的分布

    iOS程序的内存布局

    刚才说了block有三种类型,这三种block在内存中的分布如下图:

    三种block内存分配.png
    1. 上图的text区就是代码段,我们编写的代码都在代码段,代码段内存地址比较小,上图从上往下,内存地址越来越大。
    2. data区就是数据段,数据段一般都放一些全局变量。
    3. 堆:动态分配内存,需要程序员自己申请内存,也需要程序员自己管理内存。比如[NSObject alloc]或者malloc()创建的对象就是存放在堆,需要我们自己管理内存(只不过ARC不需要你管了)。
    4. 栈:系统自动分配内存,自动销毁内存。存放局部变量,系统会在{}结束之后销毁局部变量。栈的内存地址最大。

    验证内存地址由低到高:

    int age = 10;
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
        int a = 10;
        NSLog(@"数据段:age %p", &age);
        NSLog(@"堆:obj %p", [[NSObject alloc] init]);
        NSLog(@"栈:a %p", &a);
        NSLog(@"数据段:class %p", [MJPerson class]);
        }
        return 0;
    }
    

    打印:

    数据段:age 0x100002488
    堆:obj 0x10061b5e0
    栈:a 0x7ffeefbff46c
    数据段:class 0x100002438
    

    如上,如果想知道MJPerson类对象放在哪里,可以看它的地址和哪个比较接近,比较地址可知,放在数据段。

    现在我们知道了三种block在内存中的分布:

    __NSGlobalBlock__是放在数据段的,也就是和全局变量放在一起。
    __NSMallocBlock__是放在堆区的,和一般的OC对象一样的,需要我们自己管理内存。
    __NSStackBlock__是放在栈区的,和局部变量是一样的,系统自动管理内存。

    但是什么样的block才是这三种block的某一种类型呢?

    先看结论:

    block类型 环境
    __NSGlobalBlock__ 没有访问auto变量
    __NSMallocBlock__ __NSStackBlock__调用了copy
    __NSStackBlock__ 访问了auto变量

    GlobalBlock没有访问auto变量,这种类型的block都可用方法代替,不常用。

    StackBlock访问了auto变量,放在栈区,这时候block捕获了auto变量的值,然后存储在block结构体内部,栈区是系统自动管理的,所以在代码块结束之后,block内存会被销毁,这时候block结构体内部的值就是乱七八糟的了,block就会有问题,如下:

    void (^block)(void);
    void test2()
    {
        // NSStackBlock
        int age = 10;
        block = ^{
            NSLog(@"block---------%d", age);
        };
    
        NSLog(@"%@", [block class]);
    }
    
    执行方法:
    test2();
    block();
    
    打印:
    __NSStackBlock__
    block---------2634434;
    

    可以看出打印age的值,就是乱的。

    那么如何解决这个问题呢?
    可以把block从栈放到堆里面,每一种类型的block调用copy后的结果如下所示:

    block类型 副本源的配置存储域 复制效果
    __NSGlobalBlock__ 程序的数据区段 什么也不做
    __NSMallocBlock__ 引用计数器增加
    __NSStackBlock__ 从栈复制到堆

    比如,将上面NSStackBlock加个copy变成NSMallocBlock,打印就是正确的,如下:

    void test2()
    {
        // NSStackBlock ->NSMallocBlock
        int age = 10;
        block = [^{
            NSLog(@"block---------%d", age);
        } copy];
    
        NSLog(@"%@", [block class]);
       [block release]; //如果是MAC,由于放在堆区了,要自己release
    }
    

    打印:

    __NSMallocBlock__
    block---------10
    

    关于NSGlobalBlock的copy操作和NSMallocBlock的copy操作的结果可自行验证。

    Demo地址:block的类型

    相关文章

      网友评论

          本文标题:iOS-block1-底层结构、变量捕获、类型

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