浅谈Block

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)