第三十四节—Block(一)

2020-12-11  本文已影响0人  L_Ares

本文为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);
}
图2.0.0.png 图2.0.1.png 图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的深入探索。

上一篇下一篇

猜你喜欢

热点阅读