美文网首页
13 - block的认识和使用

13 - block的认识和使用

作者: iOS之文一 | 来源:发表于2021-11-09 12:32 被阅读0次

    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的认识和使用

          本文链接:https://www.haomeiwen.com/subject/sekuzltx.html