学习笔记:底层原理day08【Block】

143 阅读8分钟

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没有相关的内容

// 然后排出是栈,因为作用域不像局部变量

// 最后排出代码段,感觉不像。最后却是发现,有点像全局变量。哪里都可以用,程序未销毁一直存在

}