block的本质探索

161 阅读7分钟

常用的用来分析的指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 main.m

block本质上也是一个OC对象,它内部也有个isa指针,block是封装了函数调用以及函数调用环境的OC对象

block的内存结构

大致如下:(最简单纯粹的情况)

//不穿入任何参数的block结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp; //block方法体
    Desc = desc; //
  }
};
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
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)};

内容合起来如下图


block的捕获机制

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制 全局的变量是不会记录到block的结构体中的,局部static修饰的非oc对象会保存变量地址到block结构体中,而局部的auto类型变量,无论是否是oc对象,只会传递值,不过当时oc对象时候,穿的其实就是指针,所以调用block前对oc对象操作,会影响block内部。

1.不使用外部任何变量的block结构


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp; //block方法体
    Desc = desc; //
  }
};
  struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
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)};

不会捕获任何外部变量,block最简单干净的样子。

2.局部auto变量

2.1.非oc对象的auto变量
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_xp_j4ps6cyd58l9y306tt5875fxpt4kv4_T_main_56909c_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)};

会捕获进来,在block内部会生成一个属性,在创建block的时候会当时的a的值传入,所以在调用block前无论怎么改变这个a的值,都不会影响block中值。

2.1.oc对象的auto变量
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  MCPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MCPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  MCPerson *__strong person = __cself->person; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_xp_j4ps6cyd58l9y306tt5875fxpt4kv4_T_main_07b741_mi_1,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("name")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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

也会捕获进来,会发现block中使用了person的name属性,block捕获进来后是MCPerson *__strong person;所以在block调用之前更改了,block内部的也会更改。

此时要注意,如果block是在栈上,将不会对auto变量产生强引用;

  1. 如果block被拷贝到堆上,会调用block内部的copy函数,copy函数内部会调用 _Block_object_assign函数,_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

  2. 如果block从堆上移除,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的auto变量(release)。

2.1.static修饰的auto变量
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *c;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_c, int flags=0) : c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *c = __cself->c; // bound by copy


            NSLog((NSString *)&__NSConstantStringImpl__var_folders_xp_j4ps6cyd58l9y306tt5875fxpt4kv4_T_main_36c69a_mi_1,(*c));
        }

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

同样也会捕获进来,不同的是不是捕获值,而是地址,所以block调用之前更改变量值,会影响到block中的值。

3.全局变量

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {


            NSLog((NSString *)&__NSConstantStringImpl__var_folders_xp_j4ps6cyd58l9y306tt5875fxpt4kv4_T_main_8135e5_mi_1,d);
        }

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

全局变量是不会捕获进block中的。


block的类型

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型,注意ARC下很多情况下都会对block自动copy。

  • NSGlobalBlock ( _NSConcreteGlobalBlock ): 没有访问auto变量
  • NSStackBlock ( _NSConcreteStackBlock ) : 访问了auto变量
  • NSMallocBlock ( _NSConcreteMallocBlock ): __NSStackBlock__调用了copy

下面是block进行copy后的结果:

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

  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时
  • block作为GCD API的方法参数时

__block修饰符

__block可以用于解决block内部无法修改auto变量值的问题,__block不能修饰全局变量、静态变量(static),编译器会将__block变量包装成一个对象。下方是它的大概样子的结构体

struct __Block_byref_x_0 {
  void *__isa; //指向block的类对象
__Block_byref_a_0 *__forwarding; //指向原来的该结构体的本身
 int __flags;
 int __size;
 val; //原来的变量值
};
  1. 修饰非oc对象的auto变量
__block int a = 1;
MCBlock block2 = ^(){
    NSLog(@"%i",a);
};

//上方是使用,下方是大概实现

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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_xp_j4ps6cyd58l9y306tt5875fxpt4kv4_T_main_869314_mi_0,(a->__forwarding->a));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

此时a被封装成__Block_byref_a_0结构体,在block前对a值做更改,则会影响block的值,因为你在外部的a+=3,其实是调用(a.__forwarding->a)+=3;,外部的那个a其实是__Block_byref_a_0类型了,为什么要a.__forwarding而不是a.a后面会说到。

  1. 修饰oc对象的auto变量
struct __Block_byref_bp_0 {
  void *__isa;
__Block_byref_bp_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 MCPerson *__strong bp;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_bp_0 *bp; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_bp_0 *_bp, int flags=0) : bp(_bp->__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_bp_0 *bp = __cself->bp; // bound by ref

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_xp_j4ps6cyd58l9y306tt5875fxpt4kv4_T_main_aac850_mi_1,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)(bp->__forwarding->bp), sel_registerName("name")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->bp, (void*)src->bp, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->bp, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

相比非oc对象的auto变量的封装,多出来copy、dispse2个函数,会在block的copy、dispose的时候调用,调用时机上文已经说过。

__block的内存管理

要注意当block在栈上时,并不会对__block变量产生强引用

当block被copy到堆时会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(retain)(注意:这里仅限于ARC时会retain,MRC时不会retain)。

下方是2个block同时使用__block修饰的变量,并且都被copy的情况下的示意图:

当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的__block变量(release),下方是__block修饰的变量对2个block使用的dispose的示意图:

上文说到的为什么要a.__forwarding而不是a.a的原因: 是因为block被copy后,__forwarding是指向复制到的__block变量结构体,所以要通过forwarding去拿到真正有效的__block变量。


循环引用问题

  1. 形成的原因如图

  1. 解决办法
  • __weak解决

  • __block解决(必须要调用block)


三省:

  1. block的原理是怎样的?本质是什么?

封装了函数调用以及调用环境的OC对象

  1. __block的作用是什么?有什么使用注意点?

自己感受啦

  1. block的属性修饰词为什么是copy?使用block有哪些使用注意?
  • block一旦没有进行copy操作,就不会在堆上
  • 使用注意:循环引用问题
  1. block在修改NSMutableArray,需不需要添加__block?

不需要,因为传入的是指针