美文网首页
浅谈Block

浅谈Block

作者: 畵_460e | 来源:发表于2019-03-07 15:46 被阅读0次
    block的本质

    先看下面代码再转成C++代码之后是怎样的,xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 源文件 -o cpp文件

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
       void (^block)(void) = ^{
              NSLog(@"Hello, World!");
          };
    
          block();
        }
    

    转成的cpp文件代码

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
            // 定义block变量
            void (*block)(void) = &__main_block_impl_0(
                                                       __main_block_func_0,
                                                       &__main_block_desc_0_DATA
                                                       );
    
            // 执行block内部的代码
            block->FuncPtr(block);
        }
        return 0;
    }
    

    __main_block_impl_0这个结构体的内部又是如下:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      // 构造函数(类似于OC的init方法),返回结构体对象
      __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;
      }
    };
    

    可以知道__main_block_impl_0这个结构体有两个内部成员

    __block_impl 这个结构体是
    struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
    };
    __main_block_desc_0这个结构体内部是
    static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;// __main_block_impl_0结构体的大小
    }

    从上面总结来看block本质上也是一个OC对象,其内部也有一个isa指针,block内部是封装了函数调用及函数调用环境的OC对象,block内部的底层结构如图


    WechatIMG106.jpeg

    当block需要访问外部的变量的时候,这个时候的block底层的结构体又是如何的呢?例如

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int age = 10;// 默认是auto类型,离开作用域就销毁
            static int height = 10;
            block = ^{
                // age的值捕获进来(capture)
                NSLog(@"age is %d, height is %d", age, height);
            };
            age = 20;
            height = 20;
            block();
        }
        return 0;
    }打印的信息是age为10,height为20
    

    转成cpp文件block的内部实现就是

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

    亦可以看下图


    WechatIMG109.jpeg 可以看出相比上一个block,这个__main_block_impl_0结构体多了两个成员int age和int *height,出现这个的原因是为了保证block内部能够正常访问外部的变量,block有一个捕获机制(capture) WechatIMG108.jpeg

    可以看出如果是auto类型的局部变量,那么就是值传递进行捕获(直接将10传递给__main_block_impl_0内部的int age),static类型的局部变量那就是指针传递进行捕获(将static类型的height的指针地址传递给__main_block_impl_0内部的int *height),当全局变量是那就是直接访问,不去要进行捕获,因为始终在内存中

    竟然block本质是一个对象,那他在OC中又是什么类型呢?
    block有三种类型,分别是____NSGlobalBlock____、____NSStackBlock____和____NSMallocBlock____类型,可以通过调用Class方法或者是isa指针查看具体类型,但最终是继承制NSObject。栈是向低地址扩展的数据结构,是一块连续的内存区域,这句话的意思是栈顶上的地址和栈的最大容量是系统预先规定好的,自动管理内存,堆是向高地址扩展的数据结构,是不连续的内存区域,有程序员自己申请自己释放,手动管理内存 WechatIMG112.jpeg 这三种类型的block区别在于,如果block没有访问auto类型的变量,那么他是____NSGlobalBlock____类型,访问了auto类型变量,那么他是____NSStackBlock____,如果____NSStackBlock____类型调用了copy,那么他是____NSMallocBlock____ WechatIMG110.jpeg 每一种类型的block调用了copy之后出现的结果是 WechatIMG111.jpeg 需要注意的是ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:

    1.block作为函数返回值时
    2.将block赋值给__strong类型指针时
    3.block作为cocoa API中方法名含有usingBlock的方法参数时,如数组遍历的方法
    4.block作为GCD API的方法参数时

    前面我们写到block访问auto类型的局部变量时,会进行值捕获,那如果是block访问对象,那么情况又是怎么样的呢?
    typedef void (^MJBlock)(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            MJBlock block;
            {
                MJPerson *person = [[MJPerson alloc] init];
                person.age = 10;
                int age = 10;
                block = ^{
                    NSLog(@"---------%d", person.age);
                };
            }
            
            NSLog(@"------");
        }
        return 0;
    }
    

    转成CPP文件是这个block内部就是

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      MJPerson *__strong person;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *__strong _person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    而__main_block_desc_0和之前的不一样了

    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    }
    

    首先当block内部访问了对象类型的auto变量时,当如果block是在栈上,将不会对auto变量产生强引用,如果block拷贝到堆上时,那么就会调用__main_block_desc_0的copy函数,而copy函数内部会调用_Block_object_assign函数,这个函数会根据auto变量的修饰符(__strong、__weak、__unsafe__unretain)做出相应的操作,形成强引用或者弱引用,当block从堆上移除时,会调用__main_block_desc_0的dispose函数,这个函数内部又调用_Block_object_dispose函数,对这个变量进行release操作,这就是block访问auto类型的对象时,对这个对象的内存管理

    __block的内部实现

    当block内部需要修改外部auto类型的变量时,我们知道需要加一个__block修饰这个变量才可以被修改,这是什么原因呢?

    typedef void (^MJBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block  int age = 10;
            MJBlock block2 = ^{
                age = 30;
                NSLog(@"age is %d", age);
            };
            block2();  
            NSLog(@"%p",&age);
        }
        return 0;
    }
    

    转成cpp时,相比没有__block,实质上是__main_block_impl_0内部的int age成员变成了 __Block_byref_age_0 *age成员,这可以看出编译器会将__block变量包装成一个__Block_byref_age_0结构体类型的对象

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_age_0 *age; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __Block_byref_age_0这个结构体的内部如下, __forwarding是一个指向自己的指针,作用下面会讲到

    struct __Block_byref_age_0 {
      void *__isa;
    __Block_byref_age_0 *__forwarding;
     int __flags;
     int __size;
     int age;
    };
    
    当block在栈上时,并不会对__block变量产生强引用,当block被copy到堆时,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数会对__block变量形成强引用 WechatIMG113.jpeg
    当block从堆中移除时,会调用block内部的dispose函数,内部又会调用_Block_object_dispose,这个函数会自动释放引用的__block变量release image.png 关于__block的__forwoarding指针右下图:block在栈上时 image.png block从栈复制到堆上的图解 image.png 那么当我们打印age的地址时,到底是__main_block_impl_0这个block结构体的__Block_byref_age_0 *age成员地址呢?还是__Block_byref_age_0这个结构体内部的int age地址,我们可以模拟一下block结构体的实现,通过打印地址可以比较
    typedef void (^MJBlock) (void);
    //1004498c0
    struct __Block_byref_age_0 {
        void *__isa;//8 1004498c8
        struct __Block_byref_age_0 *__forwarding;//8   1004498d0
        int __flags;//4   //1004498d4
        int __size;//4   //1004498d8
        int age;//0x1004498d8
    };
    
    struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;
        void (*copy)(void);
        void (*dispose)(void);
    };
    
    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;
        struct __Block_byref_age_0 *age;
    };
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            __block int age = 10;
            
            MJBlock block = ^{
                age = 20;
                NSLog(@"age is %d", age);
            };
            
            struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
            
            NSLog(@"%p", &age);//0x1004498d8
        }
        return 0;
    }
    

    通过计算,打印出来的地址和__Block_byref_age_0中的int age地址时相同的,

    前面我们看到由__block 修饰的auto类型的变量的内部本质,但是__block修饰的是对象时内部实现又是怎么样的呢?其实和变量类似
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            MJPerson *person = [[MJPerson alloc]init];
           __block  MJPerson *weakPerson =person;
            
            MJBlock block = ^(){
                NSLog(@"%p",weakPerson);
            };
            block();
        }
        return 0;
    }
    

    转成CPP文件

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_weakPerson_0 *weakPerson; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    struct __Block_byref_weakPerson_0 {
      void *__isa;
    __Block_byref_weakPerson_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     MJPerson *weakPerson;
    };
    

    当如果__block变量是在栈上,将不会对指向的对象产生强引用,如果__block拷贝到堆上时,那么就会调用__main_block_desc_0的copy函数,而copy函数内部会调用_Block_object_assign函数,这个函数会根据auto变量的修饰符(__strong、__weak、__unsafe__unretain)做出相应的操作,形成强引用或者弱引用,当__block变量从堆上移除时,会调用__main_block_desc_0的dispose函数,这个函数内部又调用_Block_object_dispose函数,对这个变量进行release操作,这就是__block访问auto类型的对象时,对这个对象的内存管理

    block的循环引用问题 image.png

    相关文章

      网友评论

          本文标题:浅谈Block

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