iOS-OC-Block

135 阅读3分钟

Block

一、什么是block?

  1. block是封装了函数调用以及调用环境(上下文)的对象;
  2. 我们可以通过
clang -rewrite-objc infile.m -o outfile.cpp

命令将oc代码转成C/C++代码,可以发现block底层是一个结构体大致如下:

#import <Foundation/Foundation.h>
int main(int argc, char * argv[]) {
    
    int a = 10;
    void(^block)(void) = ^{
        NSLog(@"%d",a);
    };
    block();
}

转换结构如下:

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;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_my_mzhgbx3d20ldw1y7yx7xfs140000gn_T_main_1e0f26_mi_0,a);
    }

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)};
int main(int argc, char * argv[]) {

    int a = 10;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

因此我们可以知道block底层其实是一个结构体,里面包含了isa对象指针、FuncPtr函数指针,以及我们在block内部使用到的局部变量a等。

二、block对象类型

我们通过上述2impl.isa = &_NSConcreteStackBlock得知,该block为_NSConcreteStackBlock类型,除了_NSConcreteStackBlock block还有其他类型吗?

  1. _NSConcreteStackBlock栈Block,存储在栈的pop而销毁;
  2. _NSConcreteMallocBlock堆Block,存储在堆区需要程序员进行内存的管理;
  3. _NSConcreteGlobalBlock全局区Block,随着进程结束而销毁;

三、block变量捕获特性

block捕获变量可以按变量所处的环境和类型分为:

  1. 局部变量
  • 基础数据类型变量block对其进行值拷贝(赋值操作);
  • 静态类型变量block对其进行指针拷贝;
  • 对象类型变量block对其值进行拷贝的同时也拷贝其内存所有权修饰符;
  1. 全局变量
  • block不会对其值进行捕获,理由很简单因为全局变量的生命周期是跟随整个应用程序。

四、__block关键字

默认情况下block捕获的变量是不能进行赋值操作的,当我们需要对block捕获的变量进行赋值时需要对变量使用__block进行修饰。

  1. __block修饰符做了什么?

使用上文中提到的命令,将oc代码转写成C/C++代码可以发现,__block关键值修饰的变量被转换成一个结构体类型。


__block int a = 10;

转换成

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    
 
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

所以可以得出,当我们使用__block修改变量时变量会被包装成一个对象,其值存储在对象内部当我们对变量进行赋值/取值操作时,系统会通过a.__forwarding->a来进行操作。

  1. __forwarding的作用是什么呢?

__forwarding保证无论是在堆还是在栈上,我们访问到的都是同一份__block变量;因为当我们对block进行copy,会生成一份堆区的__block此时系统会让栈区的__block变量的__forwarding指向堆区的_block变量,堆区的__block变量的__forwarding指向自己,如下伪代码:

heapBlock = stackBlock.copy;
heapBlock.__forwarding = heapBlock
stackBlock.__forwarding = heapBlock
  1. 在mrc模式下__block修饰的变量block对其进行捕获时不会对其所有权修饰符进行捕获,言外之意就是不会对其进行持有内存管理权。

五、内存管理

1. block的copy操作

block类型copy结果
_NSConcreteGlobalBlock数据段什么也不做
_NSConcretStackBlock栈区生成一个堆区的block(_NSConcretStackBlock)
_NSConcreteGlobalBlock堆区  引用计数器+1

2. block的循环引用

2.1. 循环引用例子:

self.block = ^{
self
}

2.2. 循环引用的解决方法:

  • 在arc模式下可以使用__weak弱引用指针解决循环引用的问题
  • 在mrc下可以使用block解决循环引用,因为block不会对包装的截获的变量进行强引用,arc下会造成内存泄漏形成引用循环但是可以手动破除