13 - block的认识和使用

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方法,但是处于代理关系,不属于持有关系。

你可能感兴趣的:(13 - block的认识和使用)