Block的本质
Block本质上也是一个OC对象,它的内部有一个isa指针,block是封装了函数调用以及函数调用环境的oc对象
首先,我们利用clang 命令查看一下声明Block对应的c++代码(
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
)main.m 是文件名
转换为C++
block 声明删减一部分转化代码,如下
查找转换后的C++代码,其实block是有结构体组成的,具体形式如下:
也可以理解为
因此,这个block结构体 包含了isa 指针
再查找 __main_block_impl_0 函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
block实现的函数 的isa 指向_NSConcreteStackBlock 对象,因此说明block 是oc对象
我们还可以自定义block结构体,用于探测block 内部实现过程
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
};
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;
int age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
void (^block)(int, int) = ^(int a , int b){
NSLog(@"this is a block! -- %d", age);
NSLog(@"this is a block!");
};
struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
block(10, 10);
}
return 0;
}
为什么block内部修改变量没有效果
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block)(void) = ^{
// age的值捕获进来(capture)
NSLog(@"age is %d", age);
};
age = 20;
block();
}
return 0;
}
为什么age的值被修改了,但是打印得到的是修改之前的值呢?
没有其他参考途径,我们只有接着cpp文件
//相当于OC对象
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
//相当于OC 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
// age(_age) 相当于 把_age的值赋给age 此时所生成的block对象中的age的值就是_age,也就是传递过来的10
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
//相当于调用了__main_block_impl_0函数,并把age这个变量的值传递了过去,
// void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);
age = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
//因此在blcok 外部改变变量的值 block内部并没有发生改变
为什么 static 修饰的变量可以更改值
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
static int height = 10;
void (^block)(void) = ^{
// age的值捕获进来(capture)
NSLog(@"age is %d, height is %d", age, height);
};
age = 20;
height = 20;
block();
}
return 0;
}
//c++
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
static int height = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 20;
height = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4z_4p5xc0z55l38rrjdfyrzdxbw0000gn_T_main_2d6064_mi_1, age, (*height));
}
通过以上代码,我们发现static 修饰的变量传递的是地址,在block内部函数也访问的是(*height)而没static修饰的age则直接访问的是变量
为什么static 修饰就传递地址呢? 因为static修饰的变量 属于静态变量,首次被加载时static定义的变量被分配空间,程序结束后由系统释放.所以它在内存中是一直存在的,而被修饰的变量,在一定条件下是会被释放的,这是系统为了保持正确性,就把它的值传递过去。
放上一个别人总结的图,我觉得他能把上面的意思完全概括:
block的类型
前面说过block是oc对象,所以我们可以调用oc方法来查看它的父类,以及父类的父类
void (^block)(void) = ^{
NSLog(@"Hello");
};
NSLog(@"%@", [block class]);//__NSGlobalBlock__
NSLog(@"%@", [[block class] superclass]);//__NSGlobalBlock
NSLog(@"%@", [[[block class] superclass] superclass]);//NSBlock
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);//NSObject
所以很好的证明了block是OC对象。
但是在不同环境下,block的类型是不同的
* block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
* __NSGlobalBlock__( _NSConcreteGlobalBlock)
* __NSStackBlock__( _NSConcreteStackBlock )
* __NSMallocBlock__( _NSConcreteMallocBlock )
void (^block1)(void) = ^{
NSLog(@"Hello");
};
int age = 10;
void (^block2)(void) = ^{
NSLog(@"Hello - %d", age);
};
NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
NSLog(@"%d", age); //__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
} class]);
何为auto变量?
其实我们在定义局部变量时,系统会默认的添加 auto 关键字
// 特别注意是在MRC环境下,因为ARC环境,运行时会做一系列的操作
// Global:没有访问auto变量
void (^block1)(void) = ^{
NSLog(@"block1---------");
};
// Stack:访问了auto变量
int age = 10; // 相当于 auto int age = 10;
void (^block2)(void) = ^{
NSLog(@"block2---------%d", age);
};
NSLog(@"%@ %@", [block1 class], [block2 class]);//__NSGlobalBlock__ __NSStackBlock__
Block内部实现中(static struct __main_block_desc_0)的两种形态
第1种
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
};
**第二种**
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};
相比较两种 __main_block_desc_0 会发现 第二种(__main_block_desc_0)多出来两个函数(copy,dispose),这是因为在block引用对象变量才会出现第二种情况(因为引用对象变量时,要涉及内存管理,要确保使用的对象不会出现内存问题,因此block内部也会对其使用的对象做【return/release】)
block的copy
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况
block作为函数返回值时
将block赋值给__strong指针时
block作为Cocoa API中方法名含有usingBlock的方法参数时
block作为GCD API的方法参数时
如果block被拷贝到堆上
1、会调用block内部的copy函数
2、copy函数内部会调用_Block_object_assign函数
3、_Block_object_assign函数会根据变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(return)或弱引用
block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, person, 570425344));
//__main_block_desc_0 相比较之前没有强弱引用变量时,多出来copy dispose 函数
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};
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*/);}
如果block从堆上移除
1、会调用block内部的dispose函数
2、dispose函数内部会调用_Block_object_dispose函数
3、_Block_object_dispose函数会自动释放引用的变量(release)
__block修饰符
有时候我们要在block 中修改block之外的变量值,可以用static修改此变量,也可以设置为全局变量,但是我们有时只是临时用下此变量,没有必要做如此复杂的操作
__block可以用于解决block内部无法修改auto变量值的问题
__block不能修饰全局变量、静态变量(static)
编译器会将__block变量包装成一个对象
(分别讨论一下(基本数据类型) int 和 对象的区别)
我们都知道__block修饰的变量 可以再block内部对他进行修改,但这是为什么呢?
__block修饰的变量会在运行时,由系统封装成为 (Block_byref变量名序号)结构体类型的对象,我们在block内部和之后访问的变量 都是转换过后的类型
__block int age = 10;
// __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
__block NSObject *obj = [NSObject new];
// __attribute__((__blocks__(byref))) __Block_byref_obj_1 obj = {(void*)0,(__Block_byref_obj_1 *)&obj, 33554432, sizeof(__Block_byref_obj_1), __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"))};
HFBlock block = ^{
age = 20;
obj = nil;
// __Block_byref_age_0 *age = __cself->age; // bound by ref
// __Block_byref_obj_1 *obj = __cself->obj; // bound by ref
//
// (age->__forwarding->age) = 20;
// (obj->__forwarding->obj) = __null;
NSLog(@"age is %d", age);
};
NSLog(@"%p", &age);//对应转换后
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4z_4p5xc0z55l38rrjdfyrzdxbw0000gn_T_main_218c5b_mi_1, &(age.__forwarding->age)); 我们确定他访问的是对象中的age
转换后
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __Block_byref_obj_1 {
void *__isa;
__Block_byref_obj_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong obj;
};
通过上述代码我们得出结论,变量在声明__block后 就会转为其他对象形式,并且在声明以后用到的次变量都是__Block_byref _ 变量名 _序号 类型的
相比较基本数据类型转换的类型,对象类型的属性转换后,会多出来(__Block_byref_id_object_copy,__Block_byref_id_object_dispose)两个函数,这是因为,对象类型要涉及到内存管理,这两个函数是内存管理所用到的
为了从内存地址角度证明 我们在block外访问的变量也是转换过的类型,
typedef void (^HFBlock) (void);
struct __Block_byref_age_0 {
void *__isa;
struct __Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};
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;
struct __Block_byref_age_0 *age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
__block NSObject *obj = [NSObject new];
HFBlock block = ^{
age = 20;
obj = nil;
NSLog(@"age is %d", age);
};
struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
NSLog(@"%p", &age);//0x100425288
}
return 0;
}
我们需按照cpp代码的思路自定义几个机构体,来强制转换以此来窥看系统内部的操作,
(lldb) p/x blockImpl->age
(__Block_byref_age_0 *) $4 = 0x0000000100425270
(lldb) p/x &(blockImpl->age->age)
(int *) $5 = 0x0000000100425288
由此我们看到,block中age对象 距离访问的age的值 相差24个字节,我们再看age结构体
struct __Block_byref_age_0 {//假设这个对象的内存地址是0x0000000100425270
void *__isa;//指针8个字节
struct __Block_byref_age_0 *__forwarding;//指针8个字节
int __flags;//4个字节
int __size;//4个字节
//age 的地址是 0x0000000100425270 + 8 + 8 + 4 + 4 = 0x0000000100425288
int age;
};
这和我们的打印结果完全吻合,由此再次得到证明
__block的内存管理
当block在栈上时,并不会对__block变量产生强引用
当block被copy到堆时
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会对__block变量形成强引用(retain)
当block从堆中移除时
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的__block变量(release)
对象类型的auto变量、__block变量
当block在栈上时,对它们都不会产生强引用
当block拷贝到堆上时,都会通过copy函数来处理它们
__block变量(假设变量名叫做a)
_Block_object_assign((void)&dst->a, (void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_OBJECT/);
当block从堆上移除时,都会通过dispose函数来释放它们
__block变量(假设变量名叫做a)
_Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF*/);
对象类型的auto变量(假设变量名叫做p)
_Block_object_dispose((void)src->p, 3/BLOCK_FIELD_IS_OBJECT*/);
被__block修饰的对象类型
当__block变量在栈上时,不会对指向的对象产生强引用
当__block变量被copy到堆时
会调用__block变量内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)
如果__block变量从堆上移除
会调用__block变量内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放指向的对象(release)