OC中的block实现原理

一、block的本质

block本质上是一个OC对象,它内部也有isa指针,这个对象封装了函数的调用地址以及函数调用的环境(函数参数、返回值、捕获的外部变量等)。我们clang以后可以看一下它的底层存储结构:

clang操作(可以在命令行运行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xx.m将这个xx.m文件转成编译后的c/c++文件,然后在这个文件搜索__main_block_impl_0就可以找到这个block的结构体)

clang前:
int a = 20;
void (^block)(void) = ^{
  NSLog(@"a=%d",a);
}
clang后:
struct __main_block_impl_0 {
   struct __block_impl impl;
   struct __main_block_desc_0 *Desc;
   int a;
}

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

struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

*可见block底层就是一个结构体__main_block_impl_0
impl->isa:就是isa指针,可见它是一个OC对象。
impl->FuncPtr:是一个函数指针,也就是底层block中要执行的代码封装成了一个函数,然后用过这个指针去指向那个函数。
Desc->Block_size block占用的内存大小。
a:捕获的外部变量a,捕获后存储在block的底层结构体中。

二、block变量获取机制

1、全局变量的获取

不管是普通全局变量还是静态全局变量,block都不会捕获,因为全局变量在哪里都可以访问,所以block内部不对其进行捕获也可以执行访问,所以外部更改全局变量的值时,block内部打印的都是最新值。

1、静态局部变量的获取

静态局部变量在被block捕获后,在block内部是以 int *b;形式存储的,也就是block其实捕获的是变量b的地址,block内部通过b的地址获取或者修改b的值,所以外部更改b的值会影响block里面捕获的b的值,block里面更改b的值也会影响block外面b的值。

1、普通局部变量的获取

普通局部变量被block捕获后在block底层结构体中是以int a;形式存储的,也就是block捕获的其实是a的值,并且block内部重新定义了一个变量来存储这个值,这个时候block里面和外面的其实是2个不同的变量,所以外面更改a的值不会影响block内部的值。

三、block的类型

block有三种类型NSGlobalBlock,NSStackBlock,NSMallocBlock.

分别介绍一下:

  • NSGlobalBlock
    如果一个block里面没有访问普通的局部变量(也就是说没有访问任何外部变量或者访问的是静态局部变量或者全局变量)。那么这个block就是NSGlobalBlock。它在内存中是存放在数据区的(也叫做全局区或者静态区,全局变量和静态变量都存在这个区域),它调用copy方法的话什么也不会发生。
    *NSStackBlock
    如果一个block里面访问了普通的局部变量,那它就是一个NSStackBlock,它在内存中存储在栈区,栈区的特点就是其释放不受开发者控制,它是由系统释放操作的,所以在调用它的时候一定要保证它没有被释放。如果对一个NSStackBlock类型的block进行copy操作,那它会被复制到堆上。
    *NSMallocBlock
    一个NSStackBlock类型的block调用了copy,那会将这个block从栈复制到堆上,堆上的这个block类型为NSMallocBlock,所以NSMallocBlock类型的block是存储在堆上的。如果对一个NSMallocBlock类型的block进行copy操作,它的引用计数会+1.

四、 block对对象型的局部变量的捕获

block对对象类型和对基本数据类型变量的捕获是不一样的,对象类型的变量涉及到强引用和弱引用的问题。

如果block是在栈上,不管捕获的对象是强指针还是弱指针,block内部都不会对这个对象产生强引用。所以我们主要看block在堆上的情况。

强引用的对象在block捕获后,在结构体上多了一个修饰关键字__strong.
弱引用的对象在block捕获后,在结构体上多了一个修饰关键字__weak.

当block被拷贝到堆上的时候调用copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据这些关键字进行操作。

  • 如果关键字是__strong,那block内部会堆这个对象进行一次retain操作,引用计数+1,也就是block会强引用这个对象。也正是这个原因,导致在使用block时很容易造成循环引用。
    *如果关键子是__weak或者__unsafe__unretained,那么block对这个对象是弱引用,不会造成循环引用。所以我们通常在block外面定义一个__weak或者__unsafe__unretained修饰的弱指针指向对象,然后在block内部使用这个弱指针来解决循环引用的问题。
  • block从堆上移除时,会调用内部的dispose函数,dispose函数内部调用_Block_object_dipose函数会自动给释放强引用的变量。

五、 __block修饰符的作用

我们看一下使用__block关键字修饰后,底层到底做了什么。

- (void)test1 {
    __block int age = 10;
    void (^block)(void) = ^{
        age = 20;
    };
    block();
    NSLog(@"%d",age);
}

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
};

struct __Block_byref_age_0 {
  void *__isa; // isa指针
__Block_byref_age_0 *__forwarding; // 如果这block是在堆上那么这个指针就是指向它自己,如果这个block是在栈上,那这个指针是指向它拷贝到堆上后的那个block
 int __flags;
 int __size; // 结构体大小
 int age; // 真正捕获到的age
};

我们可以看到,age用__block修饰后,在block的结构体中变成了__Block_byref_age_0* age;而__Block_byref_age_0是一个结构体,里面有个成员int age;这个才是真正捕获到的外部变量age,实际上外部的age的地址也是指向这里的,所以不管外面还是block里面修改age时,其实都是通过地址找到这里进行修改的。
所以age用__block修饰后它就不再是一个test1方法内部的局部变量了,而是一个被包装成一个对象,age在这个对象里面存储。

你可能感兴趣的:(OC中的block实现原理)