Block
block是封装了函数调用以及函数调用环境的OC对象
。
只要实现下面的代码,llvm在编译程序时就会自动生成与该block对应的实例对象。
int main(int argc, const char * argv[]) {
^{
NSObject *obj = [NSObject new];
};
return 0;
}
上面的代码通过llvm转换,得到下面的c++代码:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// block描述信息
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类
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内的执行代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
}
int main(int argc, const char * argv[]) {
// 创建一个block实例
&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
return 0;
}
从上面的代码可以看出,当llvm进行编译时发现存在一个block时,会做以下几个操作:
- 增加一个与该block对应的结构体__main_block_impl_0。
- 增加一个该block的描述静态结构体__main_block_desc_0,同时生成一个实例__main_block_desc_0_DATA。
- 生成一个全局静态函数__main_block_func_0,内部封装了block内的执行代码。
- 在block代码处生成一个__main_block_impl_0的实例对象,并通过构造函数将__main_block_func_0(函数地址)与__main_block_desc_0_DATA(描述信息实例)传入,其中函数地址赋值给funcPtr成员变量。
注:因为__main_block_impl_0
中含有isa,所以它的实例也是OC对象。
如果将代码改成如下:
void (^testBlock)(void) = ^{
NSObject *obj = [NSObject new];
};
testBlock();
llvm转换后如下:
int main(int argc, const char * argv[]) {
void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
return 0;
}
相当于在最后再增加以下步骤:
- 将生成的__main_block_impl_0实例对象指针赋值给testBlock局部变量。此时,testBlock指向__main_block_impl_0实例对象。
- testBlock调用FuncPtr函数。
关于block转换后的对象结构如下图:
- isa:指向block的类对象(类型),block一共有三种类对象:
- _NSConcreteGlobalBlock(全局block)
- _NSConcreteStackBlock(栈block)
- _NSConcreteMallocBlock(堆block)
- reserved:保留字段。
- invoke(最新版本叫FuncPtr):函数地址。
- size:block大小。
- copy:对block执行copy操作执行的函数地址。
- dispose:当堆上的block销毁时执行的函数地址。
copy和dispose主要是处理捕获OC对象的内存管理,若未捕获OC对象,则Desc中不存在这两个字段。
在block结构的最下面是捕获到的变量。
Block 捕获变量
实现以下代码:
int globalNum = 10;
NSObject *globalObj;
int main(int argc, const char * argv[]) {
int autoNum = 10;
NSObject *autoObj;
static int staticNum = 10;
static NSObject *staticObj;
void (^testBlock)(void) = ^{
NSLog(@"%d %d %d", autoNum, staticNum, globalNum);
autoObj;
staticObj;
globalObj;
};
testBlock();
return 0;
}
通过llvm转换成C++:
int globalNum = 10;
NSObject *globalObj;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int autoNum;
int *staticNum;
NSObject *__strong autoObj;
NSObject *__strong *staticObj;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _autoNum, int *_staticNum, NSObject *__strong _autoObj, NSObject *__strong *_staticObj, int flags=0) : autoNum(_autoNum), staticNum(_staticNum), autoObj(_autoObj), staticObj(_staticObj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到无论对于基本数据类型还是实例对象指针,全局变量没有捕获,局部静态变量是指针捕获,普通局部变量是值捕获。
苹果这样设计其实很好理解,对于全局变量可以直接访问,不需要捕获;对于局部静态变量,不会被销毁,捕获指针可以追踪值的变化;对于普通局部变量,在作用域结尾将会销毁,如果是指针捕获容易造成野指针错误。
下面来看下block对实例对象的成员变量或属性的捕获:
int main(int argc, const char * argv[]) {
LJPerson2 *person = [LJPerson2 new];
void (^testBlock)(void) = ^{
person.age;
person->num;
};
return 0;
}
// llvm转换C++:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
LJPerson2 *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, LJPerson2 *__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) {
LJPerson2 *__strong person = __cself->person; // bound by copy
((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("age"));
(*(int *)((char *)person + OBJC_IVAR_$_LJPerson2$num));
}
下面再来看下对self及self的属性和成员变量的捕获:
@implementation LJPerson2
- (void)test {
void (^testBlock)(void) = ^{
self;
self.age;
_age;
};
}
@end
// llvm转换C++:
struct __LJPerson2__test_block_impl_0 {
struct __block_impl impl;
struct __LJPerson2__test_block_desc_0* Desc;
LJPerson2 *const __strong self;
//...
};
可以看到,block对局部实例对象指针或局部实例对象的成员变量的访问都是通过捕获实例对象指针来完成。
self.age,_age,self都只捕获了self(LJPerson2 *const __strong self;
)。
在上面的例子中,self是方法的隐藏参数,也是个局部变量,指向当前实例对象,所以和实例对象捕获保持同样的逻辑。比如block中访问self.age或者类的成员变量_age,都是捕获self。
下面来看下对类对象的捕获。
int main(int argc, const char * argv[]) {
void (^testBlock)(void) = ^{
[LJPerson2 alloc];
};
return 0;
}
// llvm转换C++:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//...
};
可以看到,block中以[LJPerson2 alloc];
方式访问类对象,不会进行捕获。因为LJPerson2访问类对象,本质是通过一个全局指针访问LJPerson2类对象。
下面是另一种方式在block内访问类对象:
int main(int argc, const char * argv[]) {
Class cls = [LJPerson2 class];
void (^testBlock)(void) = ^{
cls;
};
return 0;
}
// llvm转换C++:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__unsafe_unretained Class cls;
//...
};
上面的代码中,cls是个指向类对象的局部变量,因此需要捕获。
下面是通过self方式在block内访问类对象:
+ (void)test {
void (^testBlock)(void) = ^{
[self alloc];
};
}
// llvm转换C++:
struct __LJPerson2__test_block_impl_0 {
struct __block_impl impl;
struct __LJPerson2__test_block_desc_0* Desc;
const Class self;
//...
};
可以看到,self在类方法中,作为隐藏参数,是个指向类对象的局部变量,因此也需要捕获。
总结:
- block对实例对象(类对象)成员变量的访问是通过捕获实例对象(类对象)来完成。
- block根据变量类型(全局,局部static,auto)来确定是否需要捕获,如果需要捕获,是值捕获还是指针捕获。
Block类对象(类型)
Block有三种类对象,分别是:
- _NSConcreteGlobalBlock/
__NSGlobalBlock__
(全局block) - _NSConcreteStackBlock/
__NSStackBlock__
(栈block) - _NSConcreteMallocBlock/
__NSMallocBlock__
(堆block)
__NSGlobalBlock__
的继承树如下:
NSObject
NSBlock
__NSGlobalBlock
__NSGlobalBlock__
如果block没有访问auto变量,则该block类型是__NSGlobalBlock__
。
如果block访问了auto变量,则该block类型默认是__NSStackBlock__
。
对__NSGlobalBlock__
执行copy,返回的block仍然为自身,相当于什么都没做。
对__NSStackBlock__
执行copy,会创建一个__NSMallocBlock__
并返回。如果执行多次,则会创建多个__NSMallocBlock__
。
对__NSMallocBlock__
执行copy,引用计数+1,不会创建新的block。
大部分情况下,都不会使用__NSStackBlock__
,因为超过作用域就会释放。
block也是对象,__NSMallocBlock__
也需要release来释放,不过在ARC下,编译器帮我们生成好了相关释放代码。
在ARC下,将__NSStackBlock__
赋值给strong修饰的变量,会自动对其进行copy创建一个__NSMallocBlock__
,并返回给strong指针。
在ARC下,将__NSStackBlock__
作为函数返回值,也会在函数返回时自动copy。
在ARC下,NSLog输出__NSStackBlock__
,在输出log信息时也会自动进行copy。
在ARC下,下面代码的输出结果能印证上面ARC补充的操作。
typedef void (^LJBlock)(void);
LJBlock testBlock() {
int age = 10;
return ^{
NSLog(@"%d", age);
};
}
int main(int argc, const char * argv[]) {
int age = 10;
LJBlock blk = testBlock();
NSLog(@"\n%@\n %@\n %@", [testBlock() class], [^{age;} class], [blk class]);
NSLog(@"\n%@\n %@\n %@", testBlock(), ^{age;}, blk);
return 0;
}
输出结果:
__NSMallocBlock__
__NSStackBlock__
__NSMallocBlock__
<__NSMallocBlock__: 0x600002f5c000>
<__NSMallocBlock__: 0x600002f5c030>
<__NSMallocBlock__: 0x600002f01200>
block捕获auto对象的引用类型
- 在ARC和MRC下,对于
__NSStackBlock__
捕获的auto对象,无论使用weak还是strong修饰,都不会对其 增加引用计数(强引用)。
对于以下的代码,block明显是__NSStackBlock__
类型。
person未使用修饰符,ARC默认认为是__strong修饰。
LJPerson2 *person = [LJPerson2 new];
^{
person;
};
使用以下指令,以ARC格式进行转换:
xcrun -sdk iphoneos clang -arch arm64 -fobjc-arc -fobjc-runtime=ios-11.0.0 -rewrite-objc main.m -o main_arm64.cpp
得到的捕获对象如下:
// person使用__strong修饰时
struct __main_block_impl_0 {
LJPerson2 *__strong person;
};
// person使用__weak修饰时
struct __main_block_impl_0 {
LJPerson2 *__weak person;
};
使用以下指令,以MRC格式进行转换:
xcrun -sdk iphoneos clang -arch arm64 -fno-objc-arc -fobjc-runtime=ios-11.0.0 -rewrite-objc main.m -o main_arm64.cpp
得到的捕获对象如下:
struct __main_block_impl_0 {
LJPerson2 *person;
};
总结:
可以看到,捕获对象的同时也捕获了对象的引用类型修饰符(__strong,__weak,__unsafe_unretained)。但事实上,这里捕获的__strong只是简单的修饰用,最终并不会生成相关引用计数+1的代码,就像在MRC中使用strong不会导致引用计数变化。
因此,对于
__NSStackBlock__
,在ARC和MRC环境都不会强引用(retain)捕获对象。
下面来看下同时生成的copy和dispose和函数,这两个函数指针会在首次创建对象时初始化给Desc中的copy和dispose成员变量。
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*/);
}
_Block_object_assign函数调用时机及作用
当block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用_Block_object_assign函数。
_Block_object_assign函数会根据__main_block_impl_0结构体内部的person是什么类型的指针,对person对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对person进行引用计数器的操作,如果__main_block_impl_0结构体内person指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内person指针是__weak类型,则为弱引用,引用计数不变。
_Block_object_dispose函数调用时机及作用
当block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。
若在_Block_object_assign中进行了retain操作,则_Block_object_dispose会对person对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。
因此,对于
__NSMallocBlock__
,会根据捕获对象的修饰符来决定是否需要对其强引用。
下面来验证下MRC的情况:
以下代码在MRC执行,blk是__NSStackBlock__
,因此并未强引用person,person在block will release
前先释放。
int main(int argc, const char * argv[]) {
{
LJBlock blk;
{
LJPerson2 *person = [LJPerson2 new];
blk = ^{
person;
};
[person release];
}
NSLog(@"block will release");
}
return 0;
}
输出结果:
-[LJPerson2 dealloc]
block will release
以下代码在MRC执行,对__NSStackBlock__
执行copy得到__NSMallocBlock__
,并赋值给blk。blk强引用person,person在blk执行release,触发的_Block_object_dispose方法内释放。
int main(int argc, const char * argv[]) {
{
LJBlock blk;
{
LJPerson2 *person = [LJPerson2 new];
blk = [^{
person;
} copy];
[person release];
}
NSLog(@"block will release");
[blk release];
}
return 0;
}
输出结果:
block will release
-[LJPerson2 dealloc]
苹果在设计block时,考虑到栈block的生命周期一般比较短,在作用域结尾就会释放,所以并没有让栈block强引用对象。
__block修饰符
编译器会自动将__block修饰的变量封装成一个对象。
__block只能修饰auto变量,不能修饰静态变量和全局变量。
实现以下代码:
int main(int argc, const char * argv[]) {
__block int age = 10;
__block NSObject *obj1;
__block NSObject *obj2 = [NSObject new];
age = 20;
[obj1 hash];
return 0;
}
使用llvm进行C++转换:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __Block_byref_obj1_1 {
void *__isa;
__Block_byref_obj1_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj1;
};
struct __Block_byref_obj2_2 {
void *__isa;
__Block_byref_obj2_2 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *obj2;
};
int main(int argc, const char * argv[]) {
// 简化后:
__Block_byref_age_0 age = {0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
__Block_byref_obj1_1 obj1 = {0,(__Block_byref_obj1_1 *)&obj1, 33554432, sizeof(__Block_byref_obj1_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131};
__Block_byref_obj2_2 obj2 = {0,(__Block_byref_obj2_2 *)&obj2, 33554432, sizeof(__Block_byref_obj2_2), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))};
(age.__forwarding->age) = 20;
objc_msgSend((obj1.__forwarding->obj1), sel_registerName("hash"));
return 0;
}
可以看到,加上__block修饰符后,age,obj1和obj2分别封装装成__Block_byref_age_0,__Block_byref_obj1_1和__Block_byref_obj2_2对象。age,obj1和obj2分别成为了它们的成员变量。
实现下面的代码:
__block int age = 10;
^{
age = 20;
};
使用llvm进行C++转换:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
}
从结果可以看到:
- 使用__block变量,都会将该变量封装到一个对象内,后续对该变量的直接访问,都变为通过封装对象来间接访问。
- __block变量封装后的对象,被block捕获不再是值捕获,而是指针捕获。可以看到
__Block_byref_age_0 *age;
捕获的是对象指针。
栈上的block,对捕获的__block变量并不会产生强引用。上面代码例子中的age(__Block_byref_age_0结构)存储在栈区,在离开作用域将会释放。
当栈上block执行copy操作拷贝到堆上时,会将__block变量(__Block_byref_age_0)同样拷贝一份到堆上,并由新的堆block强引用它。如果栈上多个block都捕获了同一个__block变量,那么copy到堆上后,它们会强引用同一个堆上的__block变量。
堆上的__block变量,只有当堆上所有强引用它的block销毁后,它才会被销毁。
总结:
当block在栈上时,对auto变量(包括__block变量)都不会产生强引用。
当block拷贝到堆上时,__block变量也会拷贝到堆上,并对其强引用。
上面分析了__block修饰基本数据类型的情况,下面来看下__block修饰对象类型。
实现下面的代码:
__block NSObject *obj = [NSObject new];
^{
[obj hash];
};
使用llvm进行C++转换:
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong obj;
// 如果转换前是:__block NSObject __weak *obj = [NSObject new];
// NSObject *__weak obj;
};
这种情况下,obj对象的内存管理交给了封装的__block变量,而不是block对象,并在生成了关于obj对象的内存管理方法copy(__Block_byref_id_object_copy)和dispose(__Block_byref_id_object_dispose)。
当block对象copy到堆上时,会触发block对象的copy方法,并将捕获的__block变量也copy到堆上。
当__block变量copy到堆上时,同时也会触发__block变量的copy方法。若在ARC下,__block变量会对__strong修饰的obj对象强引用;在MRC下,__block变量不会对obj对象强引用,注意,这里和block捕获非__block修饰的auto对象不同。因此在MRC下,通常可以用__block解决循环引用问题。
注:__block变量与block对象是不同东西。__block变量指__Block_byref_obj_0结构,block对象指__main_block_impl_0结构。
__block forwarding
前文我们看到,对__block变量的访问都需要通过__forwarding。这是因为默认情况下__forwarding指向__block变量自身,此时的__block变量在栈上。若__block变量拷贝了一份到堆上,则将栈上__block变量的__forwarding指向堆上的__block变量。这么做的的目的是当copy发生时,让栈上__block变量处于“半废弃状态”,所有对__block变量的操作都是在堆上__block进行,这样可以达到只有一个__block变量的效果。
(age.__forwarding->age) = 20; // 而不是age->age
循环引用
在ARC下造成循环引用的场景:
LJPerson2 *person = [LJPerson2 new];
void (^myBlock)(void) = ^{
person.age;
};
person.blk = myBlock;
block捕获了__strong person对象,虽然是__strong修饰也被捕获,但并没有生成引用计数+1代码,所以此时block并没有强引用person。当block赋值给myBlock强指针,block从栈copy到堆上,并触发block copy方法,判断是strong修饰符,则对person引用计数+1(强引用)。
此时myBlock强引用了person。
下面将myBlock赋值给copy修饰的person.blk,堆上block copy仍然返回是自身,只是引用计数+1。
此时person强引用了myBlock,造成循环引用。
解除循环引用可以通过以下方式,myBlock不再会强引用了person。
LJPerson2 __weak *person = [LJPerson2 new];
LJPerson2 __unsafe_unretained *person = [LJPerson2 new];
下面介绍另一种方式原理:
__block LJPerson2 *person = [LJPerson2 new];
void (^myBlock)(void) = ^{
person.age;
person = nil;
};
person.blk = myBlock;
myBlock();
block捕获了__block变量,__block变量内捕获了__strong person对象。当block从栈copy到堆上,__block变量也从栈copy到堆上,堆block对堆__block变量强引用。同时也触发__block变量的copy方法,判断如果处于ARC环境下且person使用__strong修饰符,则进行强引用。
此时myBlock强引用__block变量,__block变量强引用了person。
下面将myBlock赋值给copy修饰的person.blk,堆上block copy仍然返回是自身,只是引用计数+1。
此时person强引用了myBlock,造成三角循环引用。
当调用myBlock()时,会触发执行block体内的person = nil。
person = nil这行代码在ARC下会被识别,因为person是__strong修饰,ARC会补充如下代码:
objc_storeStrong(person, nil) // 等同于 [person release];
person = nil;
下面用person = nil;
这里设上断点,使用反汇编查看来证明这一点:
可以看到ARC补充了objc_storeStrong方法的调用,同样的场景如果注释
person = nil;
或使用MRC是看不到这行补充代码。objc_storeStrong(person, nil)的作用等同于[person release](引用计数-1),在objc4的源码中有objc_storeStrong的具体实现。
此时,__block变量不再强引用person,打破循环引用。
这种方式需要执行block,且要主动置为nil,一般不使用。
在MRC下造成循环引用的场景:
LJPerson2 *person = [LJPerson2 new];
void (^myBlock)(void) = [^{
person.age;
} copy];
person.blk = myBlock;
[myBlock release];
[person release];
该场景与ARC类似,copy到堆上的block会对捕获的对象强引用。因为MRC不支持__weak,可以使用__unsafe_unretained解决。但__unsafe_unretained容易引起野指针错误,一般使用__block解决。
__block LJPerson2 *person = [LJPerson2 new];
与ARC不同的是,在MRC下,__block变量copy方法中,__block变量一定不会对person对象强引用,因此可以避免循环引用。