美文网首页
第三十四节—Block(一)

第三十四节—Block(一)

作者: L_Ares | 来源:发表于2020-12-11 16:44 被阅读0次

    本文为L_Ares个人写作,以任何形式转载请表明原文出处。

    准备 : libclosure-73源码文件

    一、Block的基本概念

    1. 什么是Block

    • Block是带有局部变量匿名函数
    • Block又被称作闭包
      闭包 = 一个函数(或一个指向函数的指针) + 该函数执行的外部的上下文变量(也可以叫自由变量)

    2. Block的作用

    • 保存某一段代码块,在合适的地方再进行调用。

    3. Block的定义和使用

    • 通用声明格式 : (返回值类型)(^Block块名称)(参数类型)
      • 例如 : (int)(^JDBlock)(int)
    • 通用定义格式 : ^(参数类型 参数名){函数体}
      • 例如 :^(int num){return num * 10;}
    • Block如果声明了返回值类型,则Block块内所有return的变量类型必须和声明的返回值类型一致。

    3.1 无参数、无返回值的Block

        1.无参数、无返回值的block
        //声明
        void(^block0)(void);
        //定义
        block0 = ^(void){
            NSLog(@"无参数、无返回值的Block");
        };
        //调用
        block0();
    

    3.2 无参数、有返回值的Block

        2.无参数、有返回值的block
        /** 假设返回值类型为 : int **/
        //声明
        int(^block1)(void);
        //定义
        block1 = ^{
            NSLog(@"无参数、有返回值的Block");
            return 1;
        };
        //调用
        NSLog(@"block1返回了 : %d",block1());
    

    由3.1和3.2可以看出,无参数的Block,无论是否有返回值,在其定义的时候,参数(void)可以省略不写。

    3.3 有参数、无返回值的Block

        3.有参数、无返回值的block
        /** 假设存在2个参数,且参数类型为 : int和NSString **/
        //声明
        void(^block2)(int num, NSString *value);
        //定义
        block2 = ^(int a, NSString *b){
            NSLog(@"有参数、无返回值的Block");
            NSLog(@"%d --- %@",a,b);
        };
        //调用
        block2(1,@"我是参数2");
    

    3.4 有参数、有返回值的Block

        /** 4.有参数、有返回值的block **/
        /** 假设 :
         返回值类型为 : int
         存在2个参数,且参数类型为 : int和NSString
        */
        //声明
        int(^block3)(int, NSString*);
        //定义
        block3 = ^(int a, NSString *b){
            NSLog(@"有参数、无返回值的Block");
            NSLog(@"%d --- %@",a,b);
            return a + [b intValue];
        };
        //调用
        NSLog(@"block3返回了 : %d",block3(1,@"2"));
    

    由3.3和3.4可以看出,有参数的block具有 :

    1. 声明时,可以随意命名参数名,也可以不写参数名,只写参数类型。
    2. 但是定义的时候,参数类型必须和声明中的一致,并且必须随意写明参数名。

    3.5 实际开发中的Block

    在我们实际的开发中,通常会使用typedef来定义一个Block,这时,Block就会变成一个Block类型

    • 声明格式 :
      typedef 返回值类型 (^Block类型的名称)(参数列表)
    • 定义格式 :
      Block类型对象 = ^(返回值类型)(参数列表){函数体};

    例如 :

    #import "ViewController.h"
    
    typedef int(^JDBlock)(int,int);
    
    @interface ViewController ()
    
    @property (nonatomic,copy) JDBlock jd_block;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self jd_block_basic];
    }
    
    - (void)jd_block_basic
    {
        //typedef的Block
        self.jd_block = ^int(int a, int b) {
            return a + b;
        };
        NSLog(@"JDBlock的结果 : %d",self.jd_block(1,2));
    }
    

    4. Block与外部变量的关系

    4.1 只捕获外部变量,不做修改

    1. Block是将外部变量其复制到Block自己的数据结构中。
    2. Block只针对Block内部使用的外部变量进行捕获,未被Block内部使用的则不捕获。
    3. 默认情况下,Block只能捕获,但是不能修改外部的局部变量的值。
    4. Block捕获外部的局部变量后,即使外部的局部变量发生了改变,Block捕获到的变量也依然是捕获时候的值,不会跟着改变。

    关系图 :

    图1.4.0.png

    举例 :

    - (void)jd_block_test1
    {
        int number = 8;
        void(^test0_block)(void) = ^{
            NSLog(@"number = %d",number);
        };
        test0_block();
        number = 10;
        test0_block();
    }
    

    举例结果 :

    图1.4.1.png

    4.2 捕获外部变量,并且修改

    1. 对于使用__block修饰的外部局部变量,Block在捕获后,可以修改它的值。
    2. 对于使用__block修饰的外部局部变量,Block在捕获后,可以获取它修改后的值
    3. __block的作用是复制外部局部变量的引用地址到Block的数据结构内。

    关系图 :

    图1.4.2.png

    举例 :

    - (void)jd_block_test2
    {
        __block int number = 8;
        void(^test1_block)(void) = ^{
            NSLog(@"number = %d",number);
        };
        test1_block();
        number = 10;
        test1_block();
    }
    

    举例结果 :

    图1.4.3.png

    二、Block的分类

    1. 普通Block的分类

    • 执行 :
    - (void)jd_block_test3
    {
        //1.GlobalBlock
        void(^GlobalBlock)(void) = ^{
            NSLog(@"GlobalBlock");
        };
        NSLog(@"%@",GlobalBlock);
        //2.MallocBlock && StackBlock
        int a = 10;
        void(^MallocBlock)(void) = ^{
            NSLog(@"MallocBlock - %d",a);
        };
        NSLog(@"%@",MallocBlock);
    }
    
    • ARC下执行结果 :
    图2.0.0.png
    • 非ARC下执行结果 :
    图2.0.1.png
    • 单独给文件设置非ARC模式的方法如下图 :
    图2.0.2.png

    设置-fobjc-arc就是ARC模式。
    设置-fno-objc-arc就是非ARC模式。

    • 结论 :

    普通的Block拥有3种分类 :
    1. NSGlobalBlock : 全局Block
    2. NSStackBlock : 栈Block
    3. NSMallocBlock : 堆Block

    2.Block总共分类

    打开准备的libclosure-73文件,打开data.c文件,可以看到 :

    图2.0.3.png

    Block实际上有6种分类。

    Block公有6种分类 :
    1. void * _NSConcreteStackBlock[32] = { 0 };
    2. void * _NSConcreteMallocBlock[32] = { 0 };
    3. void * _NSConcreteAutoBlock[32] = { 0 };
    4. void * _NSConcreteFinalizingBlock[32] = { 0 };
    5. void * _NSConcreteGlobalBlock[32] = { 0 };
    6. void * _NSConcreteWeakBlockVariable[32] = { 0 };

    三、Block的常见用法

    1. Block作为属性

    这个就太常见了,例子就放两个控制器之间传值,不再过多解释了。

    举例 :

    Controller1 : 
    
    #import "ViewController.h"
    #import "JDViewController.h"
    
    @interface ViewController ()
    
    @property (nonatomic,copy) NSString *str;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    }
    
    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        NSLog(@"%@",self.str);
    }
    
    - (IBAction)btnClick:(id)sender {
        JDViewController *vc = [[JDViewController alloc] init];
        __weak typeof(self)weakSelf = self;
        vc.testBlock = ^(NSString *str){
            weakSelf.str = str;
        };
        [self.navigationController pushViewController:vc animated:YES];
    }
    
    Controller2 :
    
    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    typedef void(^TestBlock)(NSString *);
    
    @interface JDViewController : UIViewController
    
    @property (nonatomic,copy) TestBlock testBlock;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "JDViewController.h"
    
    @interface JDViewController ()
    
    @property (nonatomic,weak) UIButton *button;
    
    @end
    
    @implementation JDViewController
    
    - (void)viewDidLoad {
        
        [super viewDidLoad];
        [self createButton];
    }
    
    - (void)createButton
    {
        self.view.backgroundColor = [UIColor redColor];
        UIButton *btn = [[UIButton alloc] init];
        [btn setBackgroundColor:[UIColor greenColor]];
        btn.frame = CGRectMake(80.f, 80.f, 80.f, 80.f);
        [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        [btn setTitle:@"POP" forState:UIControlStateNormal];
        [btn addTarget:self action:@selector(popBack) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:btn];
        self.button = btn;
    }
    
    - (void)popBack
    {
        self.testBlock(@"传个值回去");
        [self.navigationController popViewControllerAnimated:YES];
    }
    @end
    

    举例结果 :

    图3.1.0.png

    2. Block作为函数入参

    在很多的第三方库中,可以看到Block作为参数,比如AFNresponse代码都会写在它的block参数中,这是响应式编程的一种思想。

    建立一个JDPerson类,继承于NSObject

    举例 :

    JDPerson :
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface JDPerson : NSObject
    
    @property (nonatomic,copy) NSString *subjuect;
    
    @property (nonatomic,copy) NSString *score;
    
    - (void)study:(NSString *(^)(NSString *))work;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "JDPerson.h"
    
    @implementation JDPerson
    
    - (void)study:(NSString *(^)(NSString *))work
    {
        self.score = work(self.subjuect);
    }
    
    @end
    
    Controller :
    
    - (void)jd_block_param
    {
        JDPerson *person = [[JDPerson alloc] init];
        person.subjuect = @"数学";
        [person study:^NSString * _Nonnull(NSString * param) {
            return [NSString stringWithFormat:@"%@ = 100",param];
        }];
        NSLog(@"%@",person.score);
    }
    

    举例结果 :

    图3.2.0.png

    3. Block作为返回值

    最经典又常见的应该是绘制UI时候用到的Masonry框架,作为OC链式编程的经典框架,内部就是将Block作为返回值,达到可以一直用.语法进行设值。

    其实现原理简述 :

    1. 利用对象的Getter方法,获取对象方法。
    2. 对象方法的返回值类型是一个Block类型。
    3. Block有参数、有返回值,又因为本身就是代码块,可以执行内部的内容。
    4. Block的返回值则是当前对象。

    举例 :

    JDPerson :
    @interface JDPerson : NSObject
    - (JDPerson *(^)(id))eat;
    @end
    
    @implementation JDPerson
    - (JDPerson *(^)(id))eat
    {
        return ^JDPerson *(id param){
            NSLog(@"param is %@",param);
            return self;
        };
    }
    @end
    
    Controller :
    #pragma mark - Block作为函数返回值
    - (void)jd_block_return_value
    {
        JDPerson *person = [[JDPerson alloc] init];
        person.eat(@"食物").subjuect = @"物理";
        NSLog(@"subject is %@",person.subjuect);
    }
    

    举例结果 :

    图3.3.0.png

    四、Block的循环引用问题

    1. 循环引用的产生

    Block的循环引用代码 :

    依然利用三、Block的常见用法 --> 1.Block作为属性中的举例代码,两个控制器ViewControllerJDViewController来进行举例。

    ViewController :
    //随便加一个按钮,ViewController的rootController是UINavigationController
    - (IBAction)btnClick:(id)sender {
        JDViewController *vc = [[JDViewController alloc] init];
        [self.navigationController pushViewController:vc animated:YES];
    }
    
    JDViewController :
    
    #import "JDViewController.h"
    
    typedef void(^JDVCBlock)(void);
    
    @interface JDViewController ()
    @property (nonatomic,copy) JDVCBlock jd_vc_block;
    @property (nonatomic,copy) NSString *name;
    @end
    
    @implementation JDViewController
    - (void)viewDidLoad {
        
        [super viewDidLoad];
        [self jd_block_retain_cycle];
    }
    #pragma mark - Block循环引用
    - (void)jd_block_retain_cycle
    {
        self.name = @"JD";
        self.jd_vc_block = ^{
            NSLog(@"%@",self.name);
        };
        self.jd_vc_block();
    }
    - (void)dealloc
    {
        NSLog(@"dealloc来了");
    }
    

    看到xcode的提示,造成了循环引用。

    图4.1.0.png

    循环引用的图示 :

    图4.1.1.png

    假设存在对象A对象B

    正常的引用应该是 :

    1. 对象A对象BARC环境下,生成的时候,默认都是strong强引用。
    2. 对象A持有对象B的时候,对象A会给对象B发送一个retain的信号,对象B引用计数进行+1的操作。
    3. 对象A要使用dealloc进行析构的时候,则会向它持有的对象B发送一个release信号。对象B接收到对象A发送来的release信号,就会判断自身的引用计数是否为0。
      • 如果为0,对象B调用自身的dealloc进行析构。
      • 如果不为0,对象B无法进行析构。

    循环引用则是 :

    1. 对象A对象B互相持有,又都是默认的strong强引用。
    2. 对象A想要使用dealloc进行析构,就需要对象B向它发送release信号,将对象A自己的引用计数置为0。
    3. 对象B想要使用dealloc进行析构,也需要对象A向它发送release信号,将对象B自己的引用计数置为0。
    4. 而发送release信号的方式则是通过dealloc,偏偏对象A对象B因为互相引用,所以引用计数都不为0,也就无法调用自身的dealloc,无法向对方发送release信号,于是就造成了循环引用,二者都无法进行析构。

    2. 解决Block的循环引用

    2.1 __weak

    修改上面1中的Block的循环引用代码

    - (void)jd_block_retain_cycle
    {
        self.name = @"JD";
        __weak typeof(self)weakSelf = self;
        self.jd_vc_block = ^{
            NSLog(@"%@",weakSelf.name);
        };
        self.jd_vc_block();
    }
    

    其中,__weak typeof(self)weakSelf = self;这句代码,表达了 :

    weakSelf 持有了 self并且因为是__weak,所以他们会被存入弱引用表中。

    于是,循环引用从最开始的 :

    self ——>jd_vc_block——>self

    就变成了 :

    weakSelf——>self——>jd_vc_block——>weakSelf

    但是因为是__weak,所以weakSelfself只是存入弱引用表,没有使self引用计数增加。

    所以,当selfpop的时候,self引用计数就会变成0,会调用自身的dealloc方法进行析构变成nil :

    引用链就变成了 :

    weakSelf——>self——>nil——>jd_vc_block——>weakSelf

    weakSelf持有的就是nil而不是jd_vc_block,而jd_vc_block因为selfdealloc从而接受到了release信号,于是引用计数-1,使得jd_vc_block引用计数变为0,jd_vc_block也就可以正常的析构。

    这是一种典型的中介者模式的设计。

    2.2 __strong

    问题 :先看下面一段代码 :

    - (void)jd_block_retain_cycle
    {
        self.name = @"JD";
        __weak typeof(self)weakSelf = self;
        self.jd_vc_block = ^{
            //2秒后
            dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
            //主线程
            dispatch_queue_t main_queue = dispatch_get_main_queue();
            //2秒后,主线程再执行,但是,如果在2秒前,self就pop了
            dispatch_after(timer, main_queue, ^{
                NSLog(@"%@",weakSelf.name);
            });
        };
        self.jd_vc_block();
    }
    

    循环引用是解决了,但是如果Block内的self要在2秒后才被用到,但是2秒前,self就进行了析构,那么结果就会变成如下图 :

    图4.2.0.png

    原因 :

    1. 因为没有对象对self进行强引用的持有,所以self进行pop之后,发现自己引用计数已经为0,可以进行析构,于是就调用了自身的dealloc方法。
    2. dealloc动作就会向self所持有的所有对象全都发送一个release信号,所以,self.name也就release了,自然就变成了图中的null

    解决 :

    利用局部的__strong修饰weakSelf

    - (void)jd_block_retain_cycle
    {
        self.name = @"JD";
        __weak typeof(self)weakSelf = self;
        self.jd_vc_block = ^{
            //2秒后
            dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
            //主线程
            dispatch_queue_t main_queue = dispatch_get_main_queue();
            //在Block内部定义一个__strong修饰的strongSelf对weakSelf进行强引用持有
            __strong typeof(weakSelf) strongSelf = weakSelf;
            //2秒后,主线程再执行,但是,如果在2秒前,self就pop了
            dispatch_after(timer, main_queue, ^{
                NSLog(@"%@",strongSelf.name);
            });
        };
        self.jd_vc_block();
    }
    

    jd_vc_block的内部,让weakSelfstrongSelf强引用持有。于是引用链就变成了 :

    strongSelf——>weakSelf——>self——>jd_vc_block——>weakSelf

    于是,weakSelf——>self这对弱引用表中的一对就被strongSelf进行了强引用持有,引用计数+1,在self进行pop的时候,self引用计数-1,但是不为0,所以无法调用dealloc进行析构,需要等待strongSelf析构后,发送release信号,才会让self引用计数为0,才可以析构。

    strongSelf是属于jd_vc_block内部的局部变量,作用域只有jd_vc_block内部,完成任务就会被最近的自动释放池回收释放,也就会进行析构,这时,才会再向weakSelf——>self发送release信号,self引用计数-1,置为0,才会析构,调用dealloc向持有的jd_vc_block发送release信号,完成所有对象的析构释放。

    2.3 手动释放

    既然循环引用的造成因素是两个对象互相持有,都无法析构,无法向对方发送release信号,那么除了利用__weak让引用关系发生改变,让其中一个对象可以析构外,也可以手动的将其中一个对象置为nil,从而解决循环引用。

    - (void)jd_block_retain_cycle
    {
        self.name = @"JD";
        __block JDViewController *jd_vc = self;
        self.jd_vc_block = ^{
            dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
            dispatch_queue_t main_queue = dispatch_get_main_queue();
            dispatch_after(timer, main_queue, ^{
                NSLog(@"%@",jd_vc.name);
                jd_vc = nil;
            });
        };
        self.jd_vc_block();
    }
    

    引用链为 :

    jd_vc——>self——>jd_vc_block——>jd_vc

    虽然也是循环引用,但是在jd_vc_block内部,手动的将jd_vc置为了nil。于是self收到release信号,调用dealloc,继续发送releasejd_vc_block,也调用自己的dealloc

    这里注意,最后的self.jd_vc_block();一定要调用,否则jd_vc没有被执行,也就不为nil,也依然无法破坏掉循环引用。

    2.4 引用对象作为Block的参数

    当把引用对象作为Block的参数传入的时候,Block会将参数copy一份,放入自己的结构当中,自然也就不存在Block对引用对象进行持有,也就不存在循环引用。

    - (void)jd_block_retain_cycle
    {
        self.name = @"JD";
        self.jd_vc_block = ^(JDViewController *vc) {
            dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
            dispatch_queue_t main_queue = dispatch_get_main_queue();
            dispatch_after(timer, main_queue, ^{
                NSLog(@"%@",vc.name);
            });
        };
        self.jd_vc_block(self);
    }
    

    五、总结

    本文主要是为后面的Block深入的探索学习做一个基础的铺垫,后面将要开启Block的深入探索。

    相关文章

      网友评论

          本文标题:第三十四节—Block(一)

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