block底层原理是什么?
封装了函数调用以及调用环境的OC对象
将main.m文件转换成C++文件,当前文件夹下
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
通过分析main.cpp 我们可以看到编译后的block 。
我们可以看出block进过编译后生成一个__main_block_impl_0 的结构体,内部有一个__block_impl结构体变量,而且__block_impl内部有一个isa指针,说明block其实也是属于OC对象的,而且我们看到block内部使用的age变量,也存在于block内部,说明当block内部使用变量的时候,会将变量也传入block内部进行使用。下面我们就具体来分析下block内部的本质。
block的调用流程:
通过编译文件我们可以看到几个结构体
__block_impl :isa指针、FuncPtr 指针:指向的block需要执行的代码地址
__main_block_impl_0 :结构体内部存在一个__main_block_impl_0函数,这是属于C++的构造器函数,返回是当前的一个结构体,相当于OC里面的init函数
__main_block_func_0 : 执行block内部的需要执行的代码
__main_block_desc_0 : block的描述信,第一个参数是0,第二个是block的 sizeof 内存大小。
block定义
void(*block)(void) = ((void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, age));
//去除 强制转换类型代码 伪代码 :
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age))
//我们可以看出 block内部 是通过调用__main_block_impl_0函数 返回来的结构体,并取其地址,__main_block_func_0 和 &__main_block_desc_0_DATA, age是传递的三个参数
block对变量的捕获(可以根据上述方法,查看编译文件)
类型 | 是否捕获 | 原因 |
---|---|---|
局部变量 | 会 | 出了作用域会销毁,需要捕获保留,值传递 |
局部(全局)常量static | 会 | static创建在程序退出之前始终存在内存中,所以采用指针传递 |
全局变量 | 否 | 在当前作用域是不会销毁的,即使销毁了,block也会一起销毁,所以不需要捕获 |
self | 是 | 每个函数都有隐式参数(Class *self,SEL _cmd)所以self属于局部变量,需要捕获 |
成员变量 | 是 | 成员变量的本质就是 self->name访问,所以也是需要捕获self变量来进行访问,注意捕获的是self,而并非是_name |
函数调用 | 是 | A函数调用B函数 ([self b])会进行消息转发机制 objc_msgSend(self,SEL b,参数),所以也是捕获的self |
int global = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
static int height = 10;
void (^block)(void) = ^{
NSLog(@"age :----%d",age);
NSLog(@"height :----%d",height);
NSLog(@"global :----%d",global);
};
age = 20;
height =20;
global = 20;
block();
}
return 0;}
#interface Person :NSobject
@property (nomotic,assign)int age;
#end
#implentation Person
- (void)test { --》隐式参数- (void)test:(Person * self,SEL _cmd)
}
#end
//从上一部分我们可以看出,block内部存在外部的变量,block内部会创建相应的变量来接受外部变量,此时block内部的变量已经不是外部变量了
区别是,
auto属性(默认属性) 属于值传递 所以输出是10
static 属性是指针传递 输出是20
全局变量 不会捕获到block内部,直接调用 输出是20
局部变量因为作用域问题,aoto局部变量出了作用域会自动销毁,所以block需要及时捕获值
static局部变量 是一直储存在内存中的,所以采用指针访问。
全局变量,可以直接访问,所以不需要捕获也能访问。
self是否会捕获? 隐式参数(每个函数都会有2个默认参数 就是当前 调用者self,SEL _cmd(方法名)),所以self是属于局部变量,所以会捕获
成员变量(_name),本质是调用self->name 所以也会捕获.
block分类
不同的block分布在内存中的位置不同
类型 | 内存中位置 | 特点 |
---|---|---|
NSGlobalBlock | data段 | 没有访问auto变量,跟全局变量在一块,由系统管理 |
NSMallocBlock | 堆 | 需要手动释放,NSStackBlock 调用copy生成 |
NSStackBlock | 栈 | 访问了auto变量, 系统管理释放,超过作用域就释放 |
block-copy 操作
在ARC环境下,系统默认会对block进行copy操作的几种情况:
1.block作为函数的返回值的时候。
2.将block赋值给__strong指针时。
3.block作为cocoa API中方法名含有usingBlock的方法参数时。
4.block作为GCD API的方法参数时。
copy内部原理:当block从栈copy到堆上之后,如果存在__block、__weak、__strong修饰的对象,在__main_block_desc_0函数内部会增加copy跟dispose函数,copy函数内部会根据修饰类型对对象进行强引用还是弱引用,当block释放之后会进行dispose函数,release掉修饰对象的引用,如果都没有引用对象,将对象释放
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};
block 对象类型的auto变量捕获
1.如果block在栈上,将不会对auto变量产生强引用。
2.如果block被copy到堆上,会调用block内部的copy函数,copy内部函数会调用_Block_object_assign 函数,_Block_object_assign函数会根据auto变量的修饰符(__strong, __weak,__unsafe_unretaineaod)做出相应的操作,类似于tain(形成强引用,弱引用)
block引用对象类型的auto变量的时候,ARC会对当前对象进行内存管理操作,如果用__weak修饰的对象,不会增加其引用计数,出了作用域对象就会被释放,当用__strong修饰对象,会增加其引用计数,block执行之后会进行一次release操作。
__block 详解
我们知道 __block的修饰变量之后是就可以修改其值了,但是原理是什么呢?我们先看下代码
typedef void(^JWBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 10;
JWBlock block = ^{
age = 20;
NSLog(@"ageage---------%d",age);
};
block();
}
return 0;
}
我们转换成 C++代码之后(xcrun -sdk -iphoneos clang -arch arm64 -rewrite-objc main.m)
总结:__block修饰的auto变量,编译器会包装成一个__Block_byref_age_0(根据变量名可变)的对象类型的结构体,将指向自己的指针传递给__forwarding指针(这样做的目的是为了当多个block都使用__block修饰的变量的时候,能够始终指向堆中的变量),__Block_byref_age_0结构体内部存在的变量age才是真正__block修饰的变量,通过__Block_byref_age_0 -->__forwarding-->age改变变量的值。如果变量是NSObject对象,还会处理内存管理的问题,如上图,对象类型会生成__Block_byref_id_object_copy 跟__Block_byref_id_object_dispose这两个函数,这两个函数会对当前对象进行内存内存管理工作,下面会细讲这两个函数的作用
block循环引用问题
Person * person = [[Person alloc]init];
person.block = ^{
NSLog(@"%d",person.age);
};
原因:block内部用到了外部的auto对象,block内部实现会对person进行强引用,person的block成员变量也会对block进行强引用,当person超出作用域之后,被回收,但是此时block强引用着Person,Person强引用着block 导致无法释放,造成循环引用,内存泄漏。
一般我们希望block跟person的周期是一致的,所以最好将block内部引用person的指针换成__weak弱引用是最好的。这样就不会造成互相引用,导致内存无法释放
Person * person = [[Person alloc]init];
__weak Person * weakPerson = person;
//__weak typeof(person) weakPerson = person;
//typeof作用是保持person 跟weakPerson是相同类型的。
//也可以用__unsafe_unretained 来修饰
person.block = ^{
NSLog(@"%d",weakPerson.age);
};
区别:__weak :当指向的指针没有强指针指向的时候,会将当前对象置为nil,__unsafe_unretained:当指向的指针没有强指针指向的时候,会将当前对象内存地址不变,容易造成野指针,访问错误的情况,所以不常用。
__block : 也可以解决循环引用的问题,但是使用__block时候必须执行block,并且在block内部将对象置为nil。
面试题
- block本质是什么?
封装了函数调用以及调用环境的OC对象 - __block的作用是什么?
1.如果__block在栈上,将不会对指向的对象产生强引用。
2.如果__block被copy到堆上,会调用block内部的copy函数,copy内部函数会调用_Block_object_assign 函数,_Block_object_assign函数会根据auto变量的修饰符(__strong, __weak,__unsafe_unretaineaod)做出相应的操作,类似于retain(形成强引用,弱引用)(这里只是针对ARC时会retain,在MRC下不会进行retain操作)
3.如果变量从堆中移除,会调用block内部的dispose函数,dispose内部会调用_Block_object_dispose函数会自动释放其指向的函数 - block使用修饰词为什么用copy,注意的细节
block如果没有进行copy操作,就不会在堆上,无法控制block的生命周期,违背了block得初衷。
应避免循环引用的问题 - block在修改NSMutableArray的时候,需要增加__block么?
不需要,修改可变数组内容,只是对其内容的操作,并没有对指针方面的修改,是对数组的使用并没有重新赋值操作。