Block语法
Block可以认为是一个匿名函数。语法声明如下:
return_type (^block_name)(parameters)
例如:
double (^multiplyTwoValues)(double, double);
Block字面值的写法:
^ (double firstValue, double secondValue) {
return firstValue *secondValue;
}
上面写法省略了返回值的类型,可以显示的指出返回值类型。
typedef double (^MultiplyTwoVlaues)(double, double);
MultiplyTwoVlaues mtv = ^(double firstValue, double secondValue){
return firstValue * secondValue;
};
NSLog(@"%f", mtv(3, 4));
Block也是一个Objective-C对象,可以用于赋值,当参数传递,也可以放在集合中。
数据结构定义
block的数据结构定义:
block
对应的结构体定义如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
block实际上由6部分组成:
isa指针,所有对象都有该指针,用来指向对象相关实现。
flags,用于按位表示一些block附加信息。
reserved, 保留变量。
invoke, 函数指针,指向具体的block实现的函数调用地址。
descriptor, 表示该block的附加描述信息,主要是size的大小,以及copy和dispose函数指针。
variables,capture过来的变量,block能够访问它外部的局部变量,就是因为将这些变量复制到了结构体中。
Block分类
在Objective-C中,一共有3种类型的Block:
_NSConcreteGlobalBlock全局的静态block,不会访问任何外部变量。
_NSConcreteStackBlock 保存在栈中的block,当函数返回时会被销毁。
_NSConcreateMallocBlock 保存在堆中的block,当引用计数为0时会被销毁。
Clang
为了研究编译器是如何实现block的,需要使用clang,clang命令,可以将Objective-C的源码改写成C语言,命令如下:
clang -rewrite-objc xxx.c
Block实现
全局的静态Block实现
新建一个globalBlock.c的源文件:
#include
int main()
{
^{
printf("Hello, World!/n");
} ();
return 0;
}
在文件所在的文件夹,使用命令:clang -rewrite-objc globalBlock.c,在目录中得到一个名为globalBlock.c的文件。其中关键代码:
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("Hello, World!/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()
{
((void (*)())&;__main_block_impl_0((void *)__main_block_func_0, &;__main_block_desc_0_DATA)) ();
return 0;
}
下面具体看一下是如何实现的。__main_block_impl_0就是block的实现,从中可以看出:
一个block实际上就是一个对象,主要由 isa、 impl和 descriptor组成。
在LLVM实现中,开启ARC时。block的 isa应该是 _NSConcreteGlobalBlock类型。因为使用clang命令并没有开启ARC,所以还是 _NSConreteStackBlock类型。
impl是实际的函数指针。它指向 __main_block_func_0。其实, impl就是 invoke变量。
descriptor 是描述当前这个block的附加信息,包括大小,需要capture和dispose的变量列表等。
静态Block实现
新建一个名为 stackBlock.c的文件:
#include
int main()
{
int a = 100;
void (^stackBlock)(void) = ^{
printf("%d/n", a);
};
stackBlock();
return 0;
}
使用clang命令:
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("%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 a = 100;
void (*stackBlock)(void) = ((void (*)())&;__main_block_impl_0((void *)__main_block_func_0, &;__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock);
return 0;
}
本例中,
isa指向_NSConcreteStackBlock,这是分配在栈上的实例。
__main_block_impl_0中增加了一个变量a, 在block中引用的变量a实际是在声明block时,被复制到 __main_block_impl_0结构体中的那个变量a。所以,在block内部修改变量a的内容, 不会影响外部变量a。
__main_block_impl_0增加了变量a, 所有结构体大小变了,该结构体被写在 __main_block_desc_0中。
修改上面的代码,在变量前面添加__block关键字:
#include
int main()
{
__block int i = 100;
void (^stackBlock)(void) = ^{
printf("%d/n", i);
i = 10;
};
stackBlock();
return 0;
}
转换代码:
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref
printf("%d/n", (i->__forwarding->i));
(i->__forwarding->i) = 10;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&;dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 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()
{
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&;i, 0, sizeof(__Block_byref_i_0), 100};
void (*stackBlock)(void) = ((void (*)())&;__main_block_impl_0((void *)__main_block_func_0, &;__main_block_desc_0_DATA, (__Block_byref_i_0 *)&;i, 570425344));
((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock);
return 0;
}
代码可以看到:
源码中添加了一个名为 __Block_byref_i_0的结构体,用来保存需要capture并且修改的变量i。
在 __main_block_impl_0中引用的 __Block_byref_i_0的结构体指针,这样就可以达到修改外部变量的作用。
__Block_byref_i_0结构体中带有isa,说明也是一个对象。
我们需要负责 __Block_byref_i_0结构体相关的内存管理,所以 __main_block_desc_0中增加了copy和dispose函数指针,对于在调用前后修改相应的引用计数。
堆Block实现
NSConcreteMallocBlock类型的block通常不会再源码中直接出现,默认当block被copy的时候,才会将block复制到堆中。以下是block被copy时的代码(来自这里),在第8步,目标block类型被修改为_NSConcreteMallocBlock。
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE &; flags) == WANTS_ONE;
// 1
if (!arg) return NULL;
// 2
aBlock = (struct Block_layout *)arg;
// 3
if (aBlock->flags &; BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&;aBlock->flags);
return aBlock;
}
// 4
else if (aBlock->flags &; BLOCK_IS_GLOBAL) {
return aBlock;
}
// 5
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 6
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 7
result->flags &;= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
// 8
result->isa = _NSConcreteMallocBlock;
// 9
if (result->flags &; BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
变量的复制
对于block外部变量的引用,block默认是将其复制到其数据结构中来实现访问的。
对于__block修饰的外部变量引用,block是复制其引用地址来实现访问的。
另外,可以参考《招聘一个靠谱的iOS (下)13、14题》。
ARC,MRC 对Block类型的影响
在ARC开启的情况下,将只会有NSConcreteGlobalBlock和NSConcreteMallocBlock类型的block。原本NSConreteStackBlock被NSConcreteMallocBlock类型替代。
在Block中,如果只使用全局或静态变量或者不是用外部变量,那么Block块的代码会存储在全局区。
在ARC中
如果使用外部变量,Block块的代码会存储在堆区。
在MRC中
如果使用外部变量,Block块的代码会存储在栈区。
Block默认情况下不能修改外部变量,只能读取外部变量。
在ARC中
外部变量在堆中,这个变量在Block块内与在Block块外地址相同。
外部变量在栈中,这个变量会被copy到为Block代码块所分配的堆中。
在MRC中
外部变量在堆中,这个变量在Block块内与在Block块外地址相同。
外部变量在栈中,这个变量会被copy到为Block代码块所分配的栈中。
如果需要修改外部变量,需要在外部变量前声明__block。
在ARC中
外部变量存在堆中,这个变量在Block块内与Block块外地址相同。
外部变量存在栈中,这个变量会被转移到堆中,不是复制。
在MRC中
外部变量存在堆中,这个变量在Block块内与Block块外地址相同。
外部变量存在栈中,这个变量在Block块内与Block块外地址相同。
关于Block的面试题
使用block时什么情况会发生引用循环,如何解决?
在block内如何修改block外部变量?
使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?