iOS学习笔记【六】—— Block
key point:Block 是 Objective-C 对于闭包的实现,本质是封装了函数调用以及函数调用环境的OC对象
源码解析
/* 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;
Desc = desc;
}
};
/* 包含 Block 实际函数指针的结构体 */
struct __block_impl {
void *isa;
int Flags;
int Reserved; // 今后版本升级所需的区域大小
void *FuncPtr; // 函数指针,指向 Block 的主体部分
};
/* Block 函数部分结构体 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("myBlock\n");
}
/* Block 附加信息结构体:包含今后版本升级所需区域大小,Block 的大小*/
static struct __main_block_desc_0 {
size_t reserved; // 今后版本升级所需区域大小
size_t Block_size; // Block 大小
} __main_block_desc_0_DATA;
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
Block的存储区
- NSConcreteGlobalBlock,存储在程序的数据(.data)区域,没有用到外界变量或只用到全局变量、静态变量的block。
- NSConcreteStackBlock,存储在栈区,捕获局部变量、成员变量,且没有被强引用。如果其所属的变量作用域结束,则该 Block 和__block 变量就会被废弃。
- NSConcreteMallocBlock,存储在堆区,被强引用的栈block。可以将 Block 对象和__block 变量从栈区复制到堆区上,拷贝又分为自动拷贝和手动拷贝。
Block 的自动拷贝
MRC下需要我们手动拷贝。在使用 ARC 时,编译器会自动进行判断,自动生成将Block从栈上复制到堆上的代码:
- 1.Block是函数的返回值
- 2.Block被强引用,Block被赋值给__strong或者id类型
- 3.在方法名中含有usingBlock的Cocoa方法或者GCD的API中传递Block时
Block 的手动拷贝
通过copy 实例方法来对 Block 进行手动拷贝,对于不同的block类型,拷贝的效果也不同:
Block捕获变量
int c = 180; // 全局(静态)变量任意访问,无需捕获
- (void)useBlockInterceptLocalVariables {
int a = 10; // 栈区局部变量随作用域释放,值传递
static int b = 20; // 静态局部变量随程序释放,指针传递
void (^myLocalBlock)(void) = ^{
printf("a = %d, b = %d, c = %d\n",a, b, c);
};
}
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
** __block 修饰的普通局部变量**
当给普通局部变量a加上__block后,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, __Block_byref_b_1 *_b, 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;
printf("a = %d\n",(a->__forwarding->a));
}
在 __main_block_impl_0
结构体中可以看到:新增了成员变量 __Block_byref_a_0 类型的结构体指针 a,通过指针的形式引用,这是为了可以在多个不同的 Block 中使用 __block 修饰的变量。
__Block_byref_a_0
有 5 个部分:
- __isa:标识对象类的 isa 实例变量
- __forwarding:传入变量的地址
- __flags:标志位
- __size:结构体大小
- a:存放实变量 a 实际的值
如果对__block的修改,实际上是在修改堆上的__block变量。即__forwarding指针存在的意义就是,无论在任何内存位置,都可以顺利地访问同一个__block变量。
通过 __block 来修饰的变量,在 Block 的主体部分中改变值的原理其实是:通过指针传递的方式。
Block捕获对象
强引用对象,引用计数增加
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *__strong arrM;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *__strong _arrM, int flags=0) : arrM(_arrM) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long 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需要在__main_block_desc_0
结构体中增加的成员变量copy和dispose,以及作为指针赋值给该成员变量的__main_block_copy_0
函数和__main_block_dispose_0
函数。
- 在
__main_block_copy_0
函数内部使用_Block_object_assign
函数,相当于retain实例方法的函数,将对象类型对象赋值给结构体block的成员变量并持有该对象。调用copy函数的时机是栈上的block赋值到堆上时。
static void __main_block_copy_0(struct __main_block_impl_0 *dst,struct __main_block_impl_0 *src) {
_block_object_assign(&dst->array, src_>array, BLOCK_FIELD_IS_OBJECT);
}
- 在
__main_block_dispose_0
函数内部使用_Block_object_dispose
函数,相当于release实例方法的函数,释放赋值在block用结构体变量中的对象。调用dispose函数的时机是堆上的block被废弃时。
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
_block_object_dispose(src_>array, BLOCK_FIELD_IS_OBJECT);
}
ARC下使用__weak防止循环引用
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 void __main_block_func_0(struct __main_block_impl_0 *__cself) {
Person *__weak weakPerson = __cself->weakPerson; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_447367_mi_0,weakPerson);
}
MRC下使用__block防止循环引用
通过指针的方式来访问对象,而没有对对象进行强引用,所以不会造成循环引用。
Block循环引用
- 在block中,并不是所有的block都会造成循环引用,比如UIView动画block、Masonry添加约束block、AFN网络请求回调block等。UIView动画block不会造成循环引用是因为这是类方法,调用者对象不可能强引用一个类,所以不会造成循环引用。Masonry约束block不会造成循环引用是因为block并没有持有self。AFN请求回调block不会造成循环引用是因为在内部做了处理。
- 在block中并不只是self会造成循环引用,用下划线调用属性(如_name)也会出现循环引用,效果和使用self是一样的。
Q&A
- Block带来的问题,和delegate相比有什么好处:block使用更简单,能够直接访问上下文。容易造成循环引用,效率低,block出栈需要将使用的数据从栈内存拷贝到堆内存;delegate更安全,效率高,只是保存了一个对象指针。需要存储一些临时数据。