iOS block深入浅出
概要
block
就是带有自动变量的匿名函数。
语法结构如下:
^ 返回值类型 参数列表 表达式
其中返回值类型
为void
时可省略,同理参数列表
;
block
变量结构同C
语言函数指针类似,只是将*
换为^
符号:
int (^block)(int) = ^(int)(int a){
NSLog(@"a:%d", a);
};
与通常的变量相同,block
变量可赋值操作,也可作为函数的参数传递,也可作为返回值传递;
对于block
类型变量,通常使用typedef定义,如下:
typedef int (^Block) (int);
//上面可修改为
Block block = ^(int)(int a){
NSLog(@"a:%d", a);
}
对于截获自动变量说明,类似c
语言中的值传递拷贝:若想对截获的自动变量数值类型变量进行同步修改,需要使用__block
说明符。
原理分析
block本质
通过clang(v1100.0.33.17)
编译器自带的-rewirte-objc
选项将objc
代码转换为c++
进行分析,且都是基于ARC
模式(添加-fobjc-arc
),代码如下:
void (^block)(void) = ^{
printf("block fired\n");
};
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;
__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) {
printf("block fired\n");
}
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, const char * argv[]) {
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
先从block
实现最基本的结构体说起,其结构体信息如下:
struct __block_impl {
void *isa;//isa指针
int Flags;//标志位
int Reserved;//保留位
void *FuncPtr;//函数指针
};
其中初始化构造完成后的isa
指针指向_NSConcreteStackBlock
,FuncPtr
函数指针指向具体的block
实现;如果对runtime
原理熟悉的话,这个isa
是不是似曾相识,其指向的是类对象进而指向元对象,最后指向root object
;runtime
通过isa
指针使用c语言构造了一套完整的面向对象动态语言。为保持完整的面向对象的体系,block
也使用了isa
来指向类对象,这里指向的是_NSConcreteStackBlock
,后续会对该类对象重点分析,因此,block
其实也是一个object
对象;
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;
}
};
struct __main_block_impl_0
结构体其实就是c++
构造的public
对象,包含了__block_impl
类实例对象及struct __main_block_desc_0
结构体对象(包含了类实例对象的大小);
block
的调用函数如下,其中入参默认包含了struct __main_block_impl_0 *
类型的__cself
,与objc
和c++
中的self
和this
不谋而合,即是隐含传递了block
的实例对象;
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("block fired\n");
}
具体的block()
调用就是调用实例对象中指向的函数指针来实现;
block对象源码分析
在源码Block_private.h
中的定义如下:
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
typedef void(*BlockInvokeFunction)(void *, ...);
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
与c++
重写的结构体其实是一样的,结构图(较老版本)如下所示:
__block说明符
对于block
中具有截获的自动变量值未使用__block
说明符时,其实就是在__main_block_impl_0
中添加相应的实例成员,并通过构造函数时通过值传递捕获自动变量值;
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
printf("block fired, a=%d\n", 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, const char * argv[]) {
int a = 1;
//值传参
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);
return 0;
}
对用使用__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
printf("block fired, a=%d\n", (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};
int main(int argc, const char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
与不含__block
说明符不同之处在于:__main_block_impl_0
结构体中使用了struc __Block_byref_a_0
对象及__block
对象__main_block_copy_0
及__main_block_dispose_0
对象copy/dispose
拷贝/释放函数,替换了简单地对应的实例变量成员;并且struc __Block_byref_a_0
结构体中的成员__Block_byref_a_0 *__forwarding
初始化指向了block
实例对象成员自身(这个为啥多了该成员变量且指向自身后文阐述),如下图所示
Block_private.h
中__block
源码实现如下:
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
与c++
转换的实质的一样的;
上面截获的自动变量需要使用__block
说明符来修改其值,但对于静态变量、静态全局变量及全局变量如何呢?
可以从值传递拷贝的原理分析,block
匿名函数主要就是用于保存block
函数内部变量值用于后续调用,因此存在作用域的问题,对于静态全局变量及全局变量而言,因此不存在访问不到这些变量的情况,即block
不会主动去截获这些变量,也就不需要使用__block
说明符;但对于静态变量而言,超过作用域就无法访问,因此需要截获此类型变量的地址,具体如下:
//源码如下:
{
static int a = 1;
void (^block)(void) = ^{
printf("block fired, a=%d\n", a);
};
}
//转换后的c++代码如下
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;
}
};
block存储域
以上分析的都是局部变量类型的block
变量,其类对象类型为_NSConcreteStackBlock
,若是静态类型或者全局类型则如何,是否与变量类型存储类型一致。
直接上代码并clang
转换分析:
typedef void (^Block)(int);
Block g_block1 = ^(int count){
printf("block1, count:%d", count);
};
Block g_block2, g_block3;
int main(int argc, const char * argv[]) {
int a = 1, b = 2;
g_block2 = ^(int count) {
printf("block2, count:%d, b:%d", count, b);
};
g_block3 = ^(int count){
printf("block3, count:%d", count);
};
g_block1(a);
g_block2(a);
return 0;
}
转换后的关键结构体如下:
//g_block1对应的结构体
struct __g_block1_block_impl_0 {
struct __block_impl impl;
struct __g_block1_block_desc_0* Desc;
__g_block1_block_impl_0(void *fp, struct __g_block1_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//g_block2对应的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _b, int flags=0) : b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//g_block3对应的结构体
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
g_block2 = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, b));
g_block3 = ((void (*)(int))&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA));
都是全局变量类型block
但是其类对象类型不同,对于未截获自动变量的g_block1
则为_NSConcreteGlobalBlock
,对于需要截获自动变量的g_block2
则为_NSConcreteStackBlock
;但block
类对象类型与《objective-C 高级编程 iOS与OSX多线程和内存管理》存在差异,并且转换后的c++代码未发现引用计数相关的函数调用,如_objc_retainBlock
,带着疑惑将源码转换为汇编实现(通过xcode->Product->Perform Action->Assemble "xxxxx"
)一探究竟:
Assemble
而实际的汇编实现
block
类对象类型为_NSConcreteGlobalBlock
,并且对于ARC模式下自动添加了_objc_retainBlock/_objc_release
函数调用来实现自动引用计数,实际的xcode编译环节选项中也不存在-rewrite-objc
中间生成c++代码的过程(可能老的编译器存在),因此,clang -rewrite-objc
转换后的代码可用于参考内部实现,具体要以实际生成的汇编代码为准;
【补充说明】在ARC模式下,_NSConcreteStackBlock
类型的block
对象都变成了_NSConcreteMallocBlock
类型(官方说明中也提到Transitioning to ARC Release Notes),也可以通过demo打印block
对象以验证;
#import <Foundation/Foundation.h>
typedef void (^Block)(int);
int main(int argc, const char * argv[])
{
int a = 1;
Block blk = ^(int count){
printf("%d\n", a);
};
blk(a);
NSLog(@"%@", blk);
__weak Block blk1 = ^(int count){
printf("%d\n", count);
};
NSLog(@"%@", blk1);
__weak Block blk2 = ^(int count){
printf("%d\n", a);
};
NSLog(@"%@", blk2);
NSLog(@"%@", ^{printf("%d\n", a);});
return 0;
}
打印结果如下:
因此,对于ARC模式下,
id
类型以及对象类型变量隐含着__strong
修饰符(默认使用了Block_copy
函数拷贝),若block
变量表达式含有外部变量,则为_NSConcreteMallocBlock
;若表达式不含有外部变量,则为_NSConcreteGlobalBlock
;若使用了__weak
修饰符或者未赋值给隐含__strong
修饰符的变量时,则为_NSConcreteStackBlock
;
具体生成_NSConcreteGlobalBlock
类对象类型的block
场景如下:
- 全局
block
变量且定义表达式内部不使用应被截获的自动变量; -
block
表达式中不使用应被截获的自动变量
与之_NSConcreteStackBlock
相对应的存储类型,包括如下:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
存储类型如图:
bloc类对象存储布局
那何种场景会是_NSConcreteMallocBlock
类型呢,这个不难想象。对于上面的存储类型,若是_NSConcreteStackBlock
栈类型,若超过其作用域,则内存会被释放,即无法再使用;要是需要超过其作用域调用,则需要定义为_NSConcreteGlobalBlock
或者_NSConcreteMallocBlock
,但_NSConcreteGlobalBlock
类型受限于定义位置使用不能截获自动变量,因此_NSConcreteMallocBlock
堆类型应运而生。
借用《objective-C 高级编程 iOS与OSX多线程和内存管理》书的插图就很容易理解:
那何时
block
变量会被从栈上复制到堆上?总结如下:
-
使用
copy
方法如上面demo中使用
__weak
修饰符且表达式中会被截获自动变量作为参数传递时 -
block
作为函数参数返回时如
return ^{};
-
将
block
变量赋值给__strong
修饰符id
类型的类或者block
成员变量时对于未赋值的
block
对象默认是栈类型,(ARC
模式)赋值给__strong
修饰符id
类型的类或者block
成员变量会自动copy
到堆上; -
GCD相关的API
-
Cocoa框架方法中含有
usingBlock
时
对于手动调用copy
方法时,若重复调用会如何?
__block变量存储域
上面说明了block
变量存储域,对于其表达式中持有__block
变量时,__block
类型变量(隐含为__strong
类型)是否也会被复制到堆上?答案:是,且上文中提到的__forwarding
成员变量会指向堆中的结构体实例,因此无论栈上或者堆上的__block
变量都可以访问同一个__block
变量,如图所示:
参考
clang
官方文档也可以说明__block
变量会自动调用Block_copy()
函数拷贝到堆上:
In garbage collected environments, the
__weak
variable is set to nil when the object it references is collected, as long as the__block
variable resides in the heap (either by default or viaBlock_copy()
). The initial Apple implementation does in fact start__block
variables on the stack and migrate them to the heap only as a result of aBlock_copy()
operation.
但对于使用__weak
修饰符的__block
变量,则不会进行拷贝;
循环引用
ARC
末实现block
表达式会自动持有外部对象,若外部对象又持有该block
对象,就会导致”循环引用“问题,如图所示:
#import <Foundation/Foundation.h>
typedef void (^Block)(int);
@interface MyObject : NSObject
@property (nonatomic, strong) Block blk;
@end
@implementation MyObject
- (id)init {
self = [super init];
if (self) {
_blk = nil;
}
return self;
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
int main(int argc, const char * argv[]) {
MyObject *obj = [[MyObject alloc]init];
//方式一:使用__weak修饰符避免引用计数增加
__weak MyObject *weakObj = obj;
Block blk = ^(int count){
NSLog(@"count:%d, blk:%@", count, weakObj.blk);
//方式二:手动nil释放obj对象,解决循环引用
// NSLog(@"count:%d, blk:%@", count, obj.blk);
// obj.blk = nil;
};
NSLog(@"blk:%@", blk);
obj.blk = blk;
obj.blk(1);
return 0;
}
循环引用
解决方法:
- 使用
__weak
弱引用修饰符避免增加引用计数 -
block
表达式内部手动置持有对象为nil
小知识
xcode关闭arc
对于单个源文件关闭arc
使用fno-objc-arc
编译标志,对于整个工程则修改Objective-C Automatic Reference Counting
修改为No
;
Reference
《objective-C 高级编程 iOS与OSX多线程和内存管理》