block的本质

153 阅读5分钟

Block的本质

  1. 利用下面这条命令就可以将main.m转换成main.cpp察看底层的数据结构
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
main.m
int a = 11;
    void (^block) (void) = ^ {
    NSLog(@"test-block-%d", a);
};
block()

main.cpp
// 从这里可以看出来,传进去的是值,而不是指针
int a = 11;
void (*block) (void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a));
// 为什么可以这样调用?而不是 block.imp->FunPtr(block)
// 因为 block 是 __main_block_impl_0 类型
// 第一个参数是__block_impl,内存首地址是一样的
// 所以可以__main_block_impl_0是可以强转成__block_impl直接取值的
block->FunPtr(block);

// 会将局部变量捕捉到内部
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  // 构造函数
  __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;
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/3/29/17125987048336c5~tplv-t2oaga2asx-image.image)
  }
};

/*
封装了block执行逻辑的函数
*/
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_4bjr2jtj49b4zd1fpf8mmvq80000gn_T_main_a185df_mi_0, a);
}

struct __block_impl {
  void *isa; // 和普通对象的 isa 是一样的
  int Flags;
  int Reserved;
  void *FuncPtr; // 函数地址
};

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

由此可以得出结论,block就是封装了函数调用及其上下文的OC对象

block的内存分布

变量捕捉(Captrue)

为了保证 block 内部能够正常访问外部的变量,block 有个变量捕获机制(跨函数调用肯定要捕获的,不然另一个函数怎么访问呢),捕获的是变量类型及其修饰符

变量类型 捕获到block内部 访问方式
局部变量auto 值传递
局部变量staic 引用传递
全局变量 直接访问

因为局部变量 auto 是在栈空间的,调用完有可能访问不到他的值了,所以只能捕获他的值

而局部变量 static 调用完还可以访问到他的值,也就可以捕获指针了

而全局变量的内存是在数据段的,始终都是能访问的,也就没有必要捕获了

block 的类型

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

block类型 存放内存位置 情况 执行copy操作
NSGlobalBlock 放在数据段 没有访问auto变量 不变
NSMallocBlock 放在堆中 NSStackBlock调用copy 引用计数+1
NSStackBlock 放在栈中 访问了auto变量 变成__MallocBlock__

思考:如果在ARC环境下,如果仅捕获auto变量,为什么是__NSMacllocBlock?

block 的 copy

在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如以下情况

  • block作为函数返回值时(类似masonry中的那样)
  • block赋值给__strong指针时(这个就是为什么平时创建的一个block,看似在栈中,打印class却是mallocblock的原因)
  • 系统级别的 API,比如usingBlock、GCD等

block 捕捉对象类型的 auto 变量

  • block 内部访问了对象类型的 auto 变量时
    • 如果 block 是在栈上,将不会对 auto 变量产生强引用
    • 如果 block 在堆上,会调用内部的 copy 函数
    • 如果 block 在堆上被移除,会调用内部的dispose函数
    • __weak问题解决
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-runtime=ios-8.0.0 main.m
HXBlock block;
HXBlock block1;
{
    Person *person = [[Person alloc] init];
    __weak Person* weakPerson = person;
    block = ^ {
        [weakPerson class];
    };
    block();
}
{
    Person *person = [[Person alloc] init];
    block1 = ^ {
        [person class];
    };
    block1();
}
NSLog(@"---------------");
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

// 可以根据person的修饰符来决定对外部的person是强引用还是弱引用
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

// 相当于 release
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

上面的desc里面会增加两个方法,一个是copy,一个是dispose

也可以看出如果捕获的是对象是其类型及修饰符。也就是__weak也被捕获了。

__block

  • 想修改外部变量可以用 static、全局变量,但是会一直在内存中
  • 为了解决block内部无法修改auto变量值的问题
  • 会将 __block 变量包装成一个对象,里面有一个__forwarding指针会指向自身
  • 也就是说 __block 修饰的对象也会进行内存管理
    • 和普通的对象很接近,但是要注意__forwarding指针
    • 当copy到堆上,__forwarding会指向堆的地址
main.m
__block int a = 11;
void (^block) (void) = ^ {
    a = 20;
    NSLog(@"test-block-%d", a);
};
block();

main.cpp
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 11};

void (*block) (void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a, 570425344);
block->FuncPtr(block);

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

    (a->__forwarding->a) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_4bjr2jtj49b4zd1fpf8mmvq80000gn_T_main_52d534_mi_0, (a->__forwarding->a));
}

循环引用

Person.m
- (void)test {
    _block = {
        self.age
    }
}

由于变量捕捉的原因,所以如果在 block 中强引用了 self,而 self 也强引用了 block,所以出现了循环引用,就无法释放。

  • 解决方案
    • 使用 __weak/__unsafe_unretained 修饰 self,__weak typeof(self) weakSelf = self。这个 typeof 是编译器的特性,会自动识别你的类型,相当于 __weak Person *weakSelf = self;区别就是前者会自动置为 nil,而后者是不会的
    • 使用 __block,因为 __block 会自动把变量封装成 __Block_byref_a_0 的结构体,而当被拷贝到堆上的时候,__forwarding 指针就指向堆上的内存,在执行完 block 的时候,将 self 置为 nil,就可以解决循环引用了。不发生循环引用的前提就是一定要执行 block,且在 block中将指针断开。

思考

  1. weakSelf 和 strongSelf
__weak __typeof(self)weakSelf = self;    //1
[self.context performBlock:^{      
    [weakSelf doSomething];          //2
     __strong __typeof(weakSelf)strongSelf = weakSelf;  //3
    [strongSelf doAnotherSomething];        
}];
strongSelf 就是为了强引用 block 中捕获的弱引用变量。strongSelf 相当于只是函数内部定义的一个局部变量,他的作用就是为了保住 weakSelf 的命,不至于让 weakSelf 提前被释放。
  1. 可以使用 block 实现弱关联对象。