Debug —> Debug Workflow —> Alway Show Disassembly(汇编相关)
面试题1
Block的原理是怎样的?本质是什么?
面试题2
__block的作用是什么?有什么使用注意点?
面试题3
Block的属性修饰词为什么是copy?使用block有哪些使用注意?
面试题4
block在修改NSMutableArray,需不需要添加__block
==================================================================
最简单的一个block
^{
NSLog(@“this is a block”);
};
最简单的block的调用:加上()
^{
NSLog(@“this is a block”);
}();
int age = 10;
void(^block)(void) = ^{
NSLog(@“this is a block %d”, age);
}
// 调用block
block();
【block的本质】
Block的本质是一个OC对象,他内部也有一个isa指针。
Block是封装了函数调用以及函数调用环境的OC对象(比如block外面有一个age,那么block这个struct结构中也是含有这个age属性的,也封装了函数地址)
Block的底层结构大概是:
// 用于描述block
Struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; // block占用的内存
};
Struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; // block里面块(函数)的地址
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc; // 这是一个指针
int age; // 这是因为block外面有一个age
};
int age = 10;
void(^block)(void) = ^{
NSLog(@“this is a block %d”, age);
}
struct __main_block_impl_0 *blockStruct = (__bridge struct __mian_block_impl_0 *)block;
block(); // 打断点就可以看到上面blockStruct的里面数据。
==================================================================
继续看本质:
void(^block)(void) = ^{
NSLog(@“this is a block %d”, age);
}
block()
—> 上面的block和调用转为C++代码
// 定义block对象
void(^block)(void) = ((void (*) ())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// 执行block内部的代码
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl*)block);
====== ((void (*) ())相当于强制转换,(void *)也相当于强制转换,可以去掉======
====== (__block_impl *)相当于强制转换,(void *)也相当于强制转换,可以去掉,======
// 定义block对象【调用了__main_block_impl_0函数,传了两个参数,把最终结构的地址,赋值给block】
void(^block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
// 执行block内部的代码
(block->FuncPtr)(block);
void(^block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
【调用了__main_block_impl_0函数,传了两个参数,把最终结构的地址,赋值给block】,而__main_block_impl_0函数是定义在block的strcut里面,并且同名:
// 下面就是block的真实结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
// 下面其实是构造函数(没有返回值),相当于OC的init方法,传入一系列的参数进行构造这个block对象,返回值其实是一个结构体对象,flags有一个默认值,可以不传值。fp就是封装了block执行逻辑的函数的地址。fp又赋值给了__block_impl里面的FuncPtr
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; // isa指向的类型就是_NSConcreteStackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_desc_0_DATA这个参数是一个结构体(里面有两个参数,第二个是计算函数的大小【sizeof返回block的大小】,赋值给了desc这个参数,:
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->FuncPtr)(block);这句代码,就是调用block对象里面的函数的地址。并把block这个参数传进去
==================================================================
【block变量捕获】—> block更复杂的情况
Block的变量捕获(capture)机制:
为了保证blcok内部能够正常访问外部的变量,block有一个变量捕获机制:
变量类型
捕获到block内部
访问方式
局部变量 auto
YES
值传递
局部变量 static
YES
指针传递
全局变量
NO
直接访问
结论:为何局部变量需要捕获,而全局变量不需要捕获呢?都是因为作用域的问题,局部变量在作用域结束之后就销毁了(比如跨函数调用,需要捕获的,否则访问不到了),而全局变量不会。
Int age = 10;
void(^block)(void) = ^{
NSLog(@“%d”, age);
}
Age = 20;
block(); // age打印的是10
这时候的上面代码编译为C++之后的结果:
int age = 10;
void(^block)(void) = &__mian_block_imnpl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age
);
age = 20;
Block->FuncPtr(block);
// 由于捕获了age变量,block的struct发生了变化,多了一个age的成员,并且方法构造函数也发生了一些改变。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 这是构造函数,_age变量到时候会赋值给age,即age = _age
__mian_blcok_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的时候,age的值已经传进去了。当再次改变age的值的时候,也不会改变打印的结果了
==================================================================
如下例子:
auto int age = 10; // 离开作用域就会销毁
static int height = 10;
void(^block)(void) = ^{
NSLog(@“age= %d, height = %d”,age, height);
};
age = 20;
height = 20;
block(); // 最后打印age是10,height是20
===>. 看底层转换成c++代码 ===>
auto int age = 10;
static int height = 10;
// &height,传递的是height的地址,传入了构造函数
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_imple *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
这时候的block的struct发生了变化
struct __main_blcok_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age; // 存储的对象的值
int *height; // 存储的是指向height对象的指针
__main_block_impl_0(void *fp, struct __mian_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
【全局变量案例】
int age_ = 10;
static int height_ = 10;
int main {
void (^block) (void) = ^{
NSLog(@“age is %d, height is %d”,age_, height);;
};
age_ = 20;
height_ = 20;
block(); // 最后打印age_和height_都是20
====》
int age_ = 10;
static int height_ = 10;
struct __main_blcok_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __mian_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
【新案例】:问题,下面test方法中的self会被捕获吗?【答案,会被捕获】
@implementation MJPerson
-(void)test {
void(^block)(void) = ^{
NSLog(@“%@”,self);
};
block();
}
===》编译之后转为C++代码:
static void _I_MJPerson_test(MJPerson *self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__MJPerson__test_block_impl_0(
(void *)__MJPerson__test_block_func_0,
&__MJPerson__test_block_desc_0_DATA,
self,
570425345));
其实每个对象方法,都默认会带上self和_cmd参数的。self是方法调用者,_cmd是方法名。self是局部变量。
struct __main_blcok_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
MJPerson *self;
__main_block_impl_0(void *fp, struct __mian_block_desc_0 *desc, MJPerson *self, int flags=0) :self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
如果是下面这样(比如.h有一个@property name。
-(void)test {
void(^block)(void) = ^{
// 相当于self->_name,还是捕获的self
NSLog(@“%@”,_name);
};
block();
}
还是会捕获,但是捕获的还是Person *self
==================================================================
【Block的本质】
block就是一个OC对象
block有3种类型,可以通过调用class方法,或者isa指针查看具体类型。最终都是继承自NSBlock类型。
__NSGlobalBLock__(isa是_NSConcreteGlobalBlock)
__NSMallocBLock__(isa是_NSConcreteMallocBlock)
__NSStackBLock__(isa是_NSConcreteStackBlock)
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
__NSGlobalBLock__ : __NSGlobalBLock : NSBlock : NSObject
==================================================================
void(^block1)(void) = ^{
NSLog(@“hello”);
};
int age = 10;
void(^block2)(void) = ^{
NSLog(@“%d”,age);
};
NSLog(@“%@ %@ %@”,[block1 class], [block2 class], [^{
NSLog(@“%d”, age);
} class]);
// 打印__NSGlobalBLock__ __NSMallocBLock__ __NSStackBLock__
上面图中,越往下,内存地址越大
text段放代码,也称为代码段(放在低地址)
data段(数据段),放一些全局变量
堆是动态分配内存的,由程序员写代码进行申请,并由程序员自己管理(ARC帮我们做了很多事情,以前是需要自己释放)
栈是局部变量,系统帮忙分配内存,然后系统自动释放内存。比如作用域结束了,系统就会回收内存。
test、data我们都不用管,编译成功之后,都会由编译器自动分配好
【以下为MRC下的场景,如果是ARC,系统会帮我们做了很多操作】
结论1:只要没有访问auto变量的,都是__NSGlobalBLock__
比如如下,block1、block2、block3,调用【block class】方法,都是打印__NSGlobalBLock__类型
int a = 10;
int main {
void(^block1)(void) = ^{
NSLog(@“block1 — ”);
};
void(^block2)(void) = ^{
NSLog(@“block1 — %d”,a);
};
static int b = 10;
void(^block3)(void) = ^{
NSLog(@“block1 — %d”,b);
};
}
结论2:如果访问了auto变量,那么就是__NSStackBLock__类型。(需要先将ARC关闭)
如下block4的class就是__NSStackBLock__类型
int main {
int age = 10
void(^block4)(void) = ^{
NSLog(@“block1 —%d ”, age);
};
}
【案例】
void(^block)(void);
void test2() {
// NSStackBlock
int age = 10;
block = ^ {
NSLog(@“block ——— %d”, age);
};
}
int main {
test2()
block(); // 打印的age是乱七八糟的
}
解析:虽然block中捕获了age这个值,但是block是存在栈中的,虽然block也赋值给了全局变量,但是block是存在于栈中的。当test2方法调用完毕,block中的变量随时被回收,变成乱七八糟的东西。
===》 所以我们要尽量把block放在堆中(变成__NSMallocBLock__类型)。到时候就由我们程序员决定什么时候销毁了。(答案:__NSStackBlock__调用copy【但是__NSGlobalBlock调用copy还是Global】),也就是说,首先是访问auto变量,然后再调用copy:
void(^block)(void);
void test2() {
// NSStackBlock
int age = 10;
block = [^ {
NSLog(@“block ——— %d”, age);
} copy];
}
// 如果MRC中对block做了copy操作,那么也要做release操作,才不会造成内存泄漏
int main {
test2()
block(); // 由于调用了copy方法,返回的block不再是StackBlock,而是MallocBlock,这时候打印的是10了
}
#define AGE 10
宏并非全局变量。宏只是在编译的时候,起到替换的作用(相当于它不存在)
类对象放在内存中哪个段呢?
int age = 10;
int main {
int a = 10;
NSLog(@“数据段:age = %p”,age);
NSLog(@“栈:a = %p”,a);
NSLog(@“堆:obj = %p”,【【NSObject alloc】init】);
NSLog(@“class %p”,【MJPerson class】);
// 对比打印的地址,发现,MJPerson的类对象,是放在数据段的。也就是相当于全局变量。
// 不过我们想想,用排除法也能知道。首先排除堆,因为需要我们手动管理并且ARC没有相关的内容
// 然后排出是栈,因为作用域不像局部变量
// 最后排出代码段,感觉不像。最后却是发现,有点像全局变量。哪里都可以用,程序未销毁一直存在
}