OC底层原理探索文档汇总
主要内容:
1、block的认识
2、block的基本使用
3、变量的捕获
4、循环引用问题
1、block的认识
1.1 定义
Block是一个里面存储了指向定义block时的代码块的函数指针,以及block外部上下文变量信息的结构体,简单说就是:带有自动变量的匿名函数。
我们通常使用block传递数据。
block是一个带自动变量的匿名函数,本质是一个函数。
但是在使用上更偏向于是一个引用类型。因为block可以作为参数、可以作为返回值,还可以作为一个属性供使用。
2 block的基本使用
block的使用有三种:1)作为参数;2)作为属性;3)作为返回值
我们在使用时既可以把他当做一个类即可,但是这个类只有一个函数。其他什么都没有。
1.3.1 block的定义和调用
定义: //返回值类型 (^block的变量名)(参数类型)
申请空间: ^返回值类型(参数列表)
//定义一个block
typedef void (^addBlock2)(int num1,int num2);
//block的定义和调用
- (void)blockTest{
//1:定义block
//返回值类型 (^block的变量名)(参数类型)
//2:申请空间
//^返回值类型(参数列表)
void (^testBlock)(int num1,int num2) = ^void(int num1,int num2){
NSLog(@"num1+num2=%d",num1+num2);
};
addBlock2 block2= ^void(int num1,int num2){
NSLog(@"num1+num2=%d",num1+num2);
};
//3:调用
testBlock(1,2);
block2(3,4);
}
说明:
- 先定义一个block,之后实现block,最后调用block
- 定义block可以看做是定义一个函数指针,block实现就是函数的实现
- 之后通过函数指针直接调用block。
简写:
- 如果返回值类型为void,则在申请空间时,可以省略不写
- 如果没有参数,则在申请空间时,可以省略不写
- 在申明变量时,可以只写参数类型,不写参数
- 如果有返回值,在申请空间时也可以不写返回值类型,但是要在代码段中写上return,系统会根据我们return的类型自动判断返回值类型
1.3.2 block作为属性
设置属性
//定义一个block
typedef int (^myBlock)(int num1,int num2);
/*
block作为属性有两种,一种是先定义再设置,一种是直接定义到属性中
*/
@interface WYBlock : NSObject
@property (nonatomic,strong) NSString *name;
//作为属性,就和实例变量完全一样,block要使用copy修饰(虽然不用也行)
@property (nonatomic,copy) myBlock block1;
//也可以这么写,将block定义直接放到这里
@property (nonatomic,copy) void (^myBlock2)(int num1,int num2);
使用属性
- (void)test{
addBlock2 block = [self blockTest3];
block(10,10);
}
//block作为属性
- (void)blockTest4{
WYBlock *block = [[WYBlock alloc] init];
//定义
block.block1 = ^int(int num1, int num2) {
NSLog(@"num1+num2=%d",num1+num2);
return num1+num2;
};
block.myBlock2 = ^(int num1, int num2) {
NSLog(@"num1*num2=%d",num1*num2);
};
//调用
block.block1(1, 2);
block.myBlock2(3, 5);
}
说明:
- 定义属性有两种方式,定义和使用上也不一样。
- 一种是先定义block,之后将block当做实例变量一样的定义
- 还有一种是直接将block的定义作为属性。此时的block名称就是属性名称。
- 在属性上定义的block只是一个定义,而没有block的实现,所以需要先实现,才能再调用了。
1.3.3 block作为参数传递
block作为参数传递可以实现两个类的数据传递(因为实现和定义在不同的类中)
方法实现:
//block作为参数,提前定义好block
- (void)sumWithblock:(myBlock) block{
NSLog(@"block的结果是%d",block(3,5));
}
//临时定义block
- (void)sumWithblock2:(int (^)(int num1,int num2)) block{
NSLog(@"block2的结果是%d",block(3,5));
}
方法调用:
/*
1、传递block的实现
2、在被调用的方法里调用block
*/
- (void)blockTest2{
WYBlock *block = [[WYBlock alloc] init];
[block sumWithblock:^int(int num1, int num2) {
NSLog(@"num1*num2:%d",num1*num2);
return num1*num2;
}];
[block sumWithblock2:^int(int num1, int num2) {
return num1+num2;
}];
}
说明:
- 也有两种方式,一种是直接将已定义好的block作为参数类型,一种是在作为参数类型时定义的
- 如果是第二种方式,就需要写block名称,反正接下来会用参数。block的名称也没用
- 通过这种传参的方式就可以实现数据传递
- 我们在WYBlock类中传入参数,在ViewController类中使用,这样就可以做到了数据传递。
1.3.4 block作为返回值
typedef void (^addBlock2)(int num1,int num2);
//block作为返回值
- (addBlock2)blockTest3{
return ^void(int num1,int num2){
NSLog(@"num1+num2=%d",num1+num2);
};
}
//调用
- (void)test{
addBlock2 block = [self blockTest3];
block(10,10);
}
说明:
- 先定义一个blockTest3方法,其返回值为addBlock2,所以在该方法return时需要返回一个block。可以现在实现,也可以提前定义好的实现
- 在test中调用blockTest3,接收返回值为addBlock2类型,此时就可以直接调用了。
2 block的认识
上面我们说block其实就是一个是一个带自动变量的匿名函数,这里就进行说明。
有两个需要考虑,一个是带自动变量,一个是匿名函数。
2.1 block的匿名函数认识
2.1.1 先看下函数是什么样子的
函数定义:
typedef int (*funcPtr)(int);
获取函数指针:
//C函数实现
int func(int arg) {
return arg;
};
//C函数指针赋值
funcPtr ptr = *func;
函数指针调用:
int ret1 = ptr(10);
2.1.2 再看block的使用
block定义:
typedef int (^tmpBlock)(int arg);
获取block指针:
//block指针赋值
tmpBlock block = ^(int arg){
return arg;
};
block调用
//block调用
int ret2 = block(10);
2.1.3 对比查看
经过对比,除了函数在实现时有自己的名称func,而block没有名称,需要直接赋给一个Block指针。这就是所谓的匿名函数。
因此block本质就是一个匿名函数。我们定义的block其实是一个函数指针。而block的实现就是函数实现。
为了更方便放到一块看看
//C函数实现
int func(int arg) {
return arg;
};
typedef int (*funcPtr)(int);
typedef int (^tmpBlock)(int arg);
/*
通过C函数和block的实现对比可以发现,C函数和block的声明定义基本一样,只是在实现block时没有名称,而函数是有名称的。
*/
void niminghanshu(int arg){
//C函数指针赋值
funcPtr ptr = *func;
//C函数指针调用
int ret1 = ptr(10);
//block指针赋值
tmpBlock block = ^(int arg){
return arg;
};
//block调用
int ret2 = block(10);
NSLog(@"ret1:%d---ret2:%d",ret1,ret2);
}
2.2 block的自动变量认识
上面我们看到block本质就是一个匿名函数,但是还有一个区别于函数的特性就是自动变量。
自动变量的意思是可以捕获变量。接下来看看如何捕获变量。
普通函数使用外界的变量,变量仍然是外界的变量,并不是自己的。而block在使用外界的变量时,会将外界变量copy自己的函数中,作为自己函数的一个变量。这就是捕获变量。
- (void)testVariable{
__block int a = 10;
__block NSString *str = [[NSString alloc] init];
str = @"wy11";
NSLog(@"a1---%d---%p",a,&a);
NSLog(@"str1--%@--%p",str,str);
a = 100;
NSLog(@"a2---%d---%p",a,&a);
NSLog(@"str2--%@--%p",str,str);
void (^block)(void) = ^{
a = a+1;
str = @"wy22";
NSLog(@"a3--%d--%p",a,&a);
NSLog(@"str3--%@--%p",str,str);
};
block();
NSLog(@"a4--%d---%p",a,&a);
NSLog(@"str4--%@--%p",str,str);
}
运行结果:
2021-11-07 19:14:03.228605+0800 Block的学习[5696:1734206] a1---10---0x7ff7b9551f08
2021-11-07 19:14:03.228697+0800 Block的学习[5696:1734206] str1--wy11--0x1069ae2e8
2021-11-07 19:14:03.228762+0800 Block的学习[5696:1734206] a2---100---0x7ff7b9551f08
2021-11-07 19:14:03.228818+0800 Block的学习[5696:1734206] str2--wy11--0x1069ae2e8
2021-11-07 19:14:03.228876+0800 Block的学习[5696:1734206] a3--101--0x600002ffc338
2021-11-07 19:14:03.228933+0800 Block的学习[5696:1734206] str3--wy22--0x1069ae3a8
2021-11-07 19:14:03.229006+0800 Block的学习[5696:1734206] a4--101---0x600002ffc338
2021-11-07 19:14:03.229075+0800 Block的学习[5696:1734206] str4--wy22--0x1069ae3a8
说明:
- block修改值后会影响block修改的值
- 可以看到在block中对变量进行赋值后,变量的地址值发生了变化。说明block会捕获变量。
2.3 block的类型
block根据所在的不同区域,可以分为三种类型,存储在全局区的是全局block、存储在栈的block是栈block、存储在堆的block是对block。
2.3.1 全局block(NSGlobalBlock)
- (void)blockType{
//不使用任何数据
void (^globalBlock1)(void) = ^{
NSLog(@"quanju:%d",quanju);
};
NSLog(@"wy:globalBlock1--%@",globalBlock1);
//使用全局变量
void (^globalBlock2)(void) = ^{
NSLog(@"wy");
};
NSLog(@"wy:globalBlock2--%@",globalBlock2);
}
结果:
2021-11-07 19:26:20.011182+0800 Block的学习[6134:1745135] wy:globalBlock1--<__NSGlobalBlock__: 0x1085e9208>
2021-11-07 19:26:20.011280+0800 Block的学习[6134:1745135] wy:globalBlock2--<__NSGlobalBlock__: 0x1085e9228>
说明:
- 当一个block不使用任何数据时存储在全局区,是NSGlobalBlock
- 当一个block使用全局变量时,是NSGlobalBlock
2.3.2 堆block(NSMallocBlock)
void (^mallocBlock1)(void) = ^{
self->string = @"wy";
};
NSLog(@"wy:mallocBlock1--%@",mallocBlock1);
int a;
void (^mallocBlock2)(void) = ^{
NSLog(@"a=%d",a);
};
NSLog(@"wy:mallocBlock2--%@",mallocBlock2);
运行结果:
2021-11-07 19:38:45.370101+0800 Block的学习[6586:1757682] wy:mallocBlock1--<__NSMallocBlock__: 0x60000220f9f0>
2021-11-07 19:38:45.370194+0800 Block的学习[6586:1757682] wy:mallocBlock2--<__NSMallocBlock__: 0x600002208ed0>
2.3.3 栈block(NSStackBlock)
默认情况下block是堆block,我们可以通过__weak不对block进行强持有,就是栈block,
int b = 10;
void (^ __weak stackBlock1)(void) = ^{
NSLog(@"b=%d",b);
};
NSLog(@"wy:stackBlock1--%@",stackBlock1);
运行结果:
2021-11-07 19:38:45.370276+0800 Block的学习[6586:1757682] wy:stackBlock1--<__NSStackBlock__: 0x7ff7b9aede60>
- block直接存储在全局区,如果不使用任何数据,或者只是用全局区的数据,那么是全局block
- 如果block访问局部变量或成员变量,并进行相应拷贝,此时的block是强引用,存储在堆区。
- 如果block访问局部变量或成员变量,lock通过__weak变成了弱引用,则block存储在栈区。
3、循环引用问题
3.1 问题的出现:
block中可能会出现循环引用:
- 当block持有self,就会产生循环引用,不是所有的block都会产生循环引用
- 因为block是在这个类的内部,被self使用,而block又使用了self,就会相互引用,互相等待对方先释放,造成循环引用
请看下这个代码有没有循环引用
/*
会出现循环引用
在block内部使用self会出现循环引用,因为self和block相互引用
*/
- (void)circularTest2{
self.block1 = ^int(int num1, int num2) {
self.name = @"zhang";
return num1+num2;
};
self.block1(10,2);
}
说明:
- 有循环引用
- self持有block1,block中又持有self,所以导致了self和block的相互持有
请看下这个代码有没有循环引用
/*
不会出现循环引用
虽然block使用了self,但是这个block并没有被self持有,所以不会出现
*/
- (void)circularTest{
[self sumWithblock:^int(int num1, int num2) {
self.name = @"zhang";
return num1+num2;
}];
}
说明:
- 没有循环引用
- block并没有被self持有,而是被sumWithblock持有,所以不构成相互持有。
3.2 循环引用的解决
block的循环引用归根结底就是断开其中的一个持有,打破相互持有。共有四种方案可以实现
【方案一】:使用__weak
【方案二】:手动释放一个引用
【方案三】:将self作为参数
【方案四】:使用NSProxy虚拟类
3.2.1 给self使用__weak
- 打破self对block的强引用,不再相互持有
- 持有的weakSelf是在一张弱引用表,而不是直接持有的self
- 所以就self不会计数+1,也就不会进行相互持有
/*
循环引用解决1: __weak弱引用self
将block持有self这一环断开
*/
- (void)circularTest3{
__weak typeof(self) weakSelf = self;
self.block1 = ^int(int num1, int num2) {
weakSelf.name = @"zhang";
return num1+num2;
};
self.block1(10,2);
}
注意:
- 如果block内部嵌套block,需要同时使用__weak和__strong
3.2.2 手动释放对象
将self赋给一个变量,这样就是强引用,之后我们在block执行结束后主动设置为nil,也就是主动释放掉,这样就打破了block对self的引用
/*
循环引用解决2:在block内将对象设置为nil
通过wyBlock作为中介,给self增加一个引用,之后将wyBlock设置为nil就可以给self减少一个引用计数了
*/
- (void)circularTest4{
__block WYBlock *wyBlock = self;
self.block1 = ^int(int num1, int num2) {
wyBlock.name = @"zhang";
wyBlock = nil;
return num1+num2;
};
self.block1(10,2);
}
- 对象需要被__block修饰,因为只有这样才可以修改。
- 这里的block必须被调用,如果不调用,blcok永远不会置空,这样self和block都无法被释放。
3.2.3 将self作为参数传递
/*
循环引用解决3:对象self作为参数
wyBlock的生命周期仅在block内部,与self无关。block不持有self
*/
- (void)circularTest5{
self.block11 = ^(WYBlock *wyBlock){
wyBlock.name = @"wy";
NSLog(@"wy--%@",wyBlock.name);
};
self.block11(self);
}
说明:
- wyBlock的生命周期仅在block内部,block结束后wyBlock也就销毁了
- 没有与self进行绑定,所以也就不会相互持有了
3.2.4 使用NSProxy虚基类实现
虚基类本质上是一个定义了消息转发功能的抽象类。也就是说他可以实现消息转发功能。
因此我们在这里可以通过虚基类来调用,避免了self的强引用。
详细的虚基类的认识可以查看博客:NSproxy虚基类实现代理和多继承以及多态
/*
循环引用解决4:通过虚基类调用方法,不与self绑定
*/
- (void)circularTest6{
WYProxy *proxy = [WYProxy alloc];
[proxy transformObjc:self];
self.block1 = ^int(int num1, int num2) {
[proxy performSelector:@selector(eat)];
return num1+num2;
};
self.block1(10,2);
}
- (void)eat{
NSLog(@"eat");
}
运行结果:
2021-11-09 09:51:15.464526+0800 Block的学习[31084:410195] eat
说明:
- 此处使用虚基类来调用eat方法,没有采用self来调用,所以没有持有self
- 虚基类通过代理self可以实现eat方法,但是处于代理关系,不属于持有关系。