关于block--你想了解的几乎都在这里了

一.block定义
二.block的本质
三.block变量捕获(Capture)
四.block的类型
五.block的copy操作
六.block使用了对象类型的auto变量的分析
七.__block讲解
八.block的循环引用问题
九.block的一些使用场景

block在我们开发中随处可见,比如我们常用的AFN框架和ReactiveCocoa框架等,多线程中的GCD,各种方法的回调等等,可见block在我们开发中的重要性,今天我就详细的整理下关于block的一些东西,如果有什么问题也欢迎大家一起讨论.

一. block知识回顾

1. 定义

这里借用网上的一张截图,感觉还是比较详细的

关于block--你想了解的几乎都在这里了_第1张图片

上图声明的block类型为:int (^)(int)

注意:block块中的代码并没有执行,只有当我们明确调用的时候才会具体执行

我们可以使用inlineBlock的快捷方式生成一段block代码

2. 使用typedef重定义

对于可能需要重复地声明多个相同返回值相同参数列表的block变量, 如果我们总是重复地编写一长串代码来声明变量 那么会非常的繁琐,所以此时我们可以使用typedef来对block进行重定义 具体如下:

typedef int(^sumBlock)(int , int);

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    sumBlock myblock = ^(int a,int b)
    {
        return a+b;
    };
    myblock(20,30);
}

二. block的本质

block使用起来非常的方便,那么他的底层或者说是本质到底是什么呢? 下面我们先通过一个最简单的block来分析下它的底层到底是如何实现的:

#import 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void(^block)(void) = ^{
            NSLog(@"Hello World");
        };
        
        block();
    }
    return 0;
}

通过下面的命令将我们的.m文件转化成.cpp文件

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

生成的C++文件如下图所示,为了便于理解我去掉了部分强制转换的东西:

关于block--你想了解的几乎都在这里了_第2张图片

通过以上的代码分析我们我们可以得出一个结论:

block本质上是一个OC对象,它的内部也有一个isa指针

当然我们也可以通过下面的代码来验证我们的结论:

        void (^block)(void) = ^{
            NSLog(@"Hello World");
        };
        NSLog(@"%@",[block class]);
        NSLog(@"%@",[[block class] superclass]);
        NSLog(@"%@",[[[block class] superclass] superclass]);
        NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);

执行结果:

 block[4176:730867] __NSGlobalBlock__
 block[4176:730867] __NSGlobalBlock
 block[4176:730867] NSBlock
 block[4176:730867] NSObject

重点: 可以看到block对象是继承自NSObject的一个OC对象

四. 变量捕获(Capture)

1. 局部变量auto

因为上面定义的block太过简单,一些本质的东西我们无法看到,这里我们定义一个稍微复杂一点的block对象,代码如下:

int age = 10;//等价于 auto int age = 10;
void(^block)(void) = ^{
    NSLog(@"age is %d",age);
};
age = 20;
block();

执行该block后打印的age的值是多少呢?

相信大部分同学立马就可以回答出来结果,但是为什么是这个结果呢? 相信大部分同学应该挺迷惑的

不着急我们一步步来为大家分析:

同样将该段代码转化为C++代码 如下图:

关于block--你想了解的几乎都在这里了_第3张图片

通过以上代码的简单分析我们就可以清晰的看出block在内部捕获了age,并且是 值传递, 所以age在block外部修改并不会影响到其内部的值
该段C++代码结构其实可以简化成下面这幅图:

关于block--你想了解的几乎都在这里了_第4张图片
2. 局部变量Static

下面我们来看一个稍微复杂点的代码:

        int age = 10;//等价于 auto int age = 10 ,auto可以省略
        static int height = 10;
        void(^block)(void) = ^{
            NSLog(@"age is %d,height is %d",age,height);
        };
        age = 20;
        height = 20;
        block();

执行该block后打印的结果是多少呢?
老规矩,同样将该段代码转化为C++代码,如下图所示:


关于block--你想了解的几乎都在这里了_第5张图片

通过以上代码我们可以看到block可以同时捕获到age和height,但是用Static修饰的height和age是不一样的,age是一个值传递,而height是一个引用传递,所以在block外部即使height的数据变更我们也可以获取到最新的数据.

思考

那么不知道大家有没有想过这样一个问题,为什么用static修饰的变量是引用传递,而用auto修饰的变量就是值传递呢?
下面我们通过一段简单的代码来为大家分下一下:

关于block--你想了解的几乎都在这里了_第6张图片

因为作用域的问题 当代码执行到23行的时候也就是我们执行完test函数后,此时age的内存已经销毁,如果block不马上将age的值捕获到其内部,那么执行到24行的时候,访问到的age此时就是一个垃圾数据了,而用static修饰的height就不一样了,所以传递的是地址,将来height的数据即使更新了,我们也可以获取到最新的数据.

3. 全局变量

上面的两种情况都是局部变量,下面我们在来看看全局变量的情况:

#import 

int age = 10;
static int height = 10;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void (^block)(void) = ^{
            NSLog(@"age is %d,height is %d",age,height);
        };
        age = 20;
        height = 20;
        block();
        
    }
    return 0;
}

打印结果:

age is 20,height is 20

转化为C++代码:

关于block--你想了解的几乎都在这里了_第7张图片

通过上面代码我们可以看到 __main_block_impl_0内部并没有捕获age和height 所以打印结果就是最新的数据

通过上面的分析我们应该知道因为age和height都是全局变量,在任何地方都可以访问到他们,所以block并不用去捕获他们

4. 总结:

关于block捕获变量我们可以总结成下面一个表格,清晰明了:


关于block--你想了解的几乎都在这里了_第8张图片

五.block的类型

1. block的三种类型

先说结论:block对象有3种类型,分别为:

__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__  ( _NSConcreteStackBlock  )
__NSMallocBlock__ ( _NSConcreteMallocBlock )

他们都是继承自NSBlock类型.我们都知道应用程序在内存中分为 代码段 数据段 堆段栈段,其中堆段是我们程序员自己动态分配的,并且需要我们手动释放内存(free()或者release()操作),而栈段是非常危险的,随时都有可能会被系统销毁.

因为下图比较经典我就直接拿过来用了


关于block--你想了解的几乎都在这里了_第9张图片

那么这三种类型之间如何区分以及有什么区别呢?下面我们先来看一幅图:


关于block--你想了解的几乎都在这里了_第10张图片

2. 结论一:如果block没有访问auto变量则其类型为:__NSGlobalBlock

对于这种类型的block我们不需要考虑作用域的问题,而且对他进行copy或者retain操作也是无效的.

验证:

#import 
int height = 20;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        //没有访问任何变量
        void (^block1)(void) = ^{
            NSLog(@"Hello World");
        };
        //访问全局变量
        void (^block2) (void) =  ^{
            NSLog(@"height is %d",height);
        };
        //访问static修饰的局部变量
        static int age = 10;
        void (^block3) (void) =  ^{
            NSLog(@"age is %d",age);
        };
        
        NSLog(@"%@--%@---%@",[block1 class],[block2 class],[block3 class]);
        
    }
    return 0;
}

打印结果:

block[4486:803768] __NSGlobalBlock__--__NSGlobalBlock__---__NSGlobalBlock__

3. 结论二:如果block访问了auto修饰的变量则其类型为:__NSStackBlock

验证:

int age = 10;
        void (^block) (void) =  ^{
            NSLog(@"age is %d",age);
        };
        NSLog(@"%@",[block class]);

打印结果:

block[4524:811710] __NSMallocBlock__

恩? 怎么是__NSMallocBlock__?搞错了吧.....

不着急,因为我们的项目是ARC环境,编译器帮助我们做了一些事情,我们将代码改成MRC环境:


关于block--你想了解的几乎都在这里了_第11张图片

再次执行:

block[4577:821982] __NSStackBlock__

这才对嘛

但是因为__NSStackBlock__的内存是分配在栈段的,所以在使用的时候经常会出现一些问题,比如:

#import 
void (^block) (void);
void test()
{
    int age = 10;
    block = ^{
        NSLog(@"age is %d",age);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();        
    }
    return 0;
}

执行结果:

block[4919:908731] age is -272632360

纳尼?!!! 怎么是这么个乱七八糟的数据?

因为该block的类型是__NSStackBlock__,所以当test()函数执行完毕后,block对象中成员已经被释放掉了,所以当我们在执行block()的时候拿到的数据就是垃圾数据了,而不是我们想要的10.

那么该如何解决该问题呢?
我们只需要将block进行copy操作即可.

void test()
{
    int age = 10;
    block = [^{
        NSLog(@"age is %d",age);
    } copy];
}

执行结果:

block[5004:929720] age is 10

所以在开发中,我们经常会 对 block进行Copy操作 原因就是因为这个

4. 所以结论三 NSStackBlock 调用了copy后会变成 NSMallocBlock

每一种类型的block调用Copy操作后的结果如下图所示:


关于block--你想了解的几乎都在这里了_第12张图片

block的copy操作

在上文中我们的环境是MRC环境,如果是ARC环境的时候 block内部使用了auto修饰的局部变量时,该block的类型是NSMallocBlock,为什么会这样呢?因为在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下这些情况:

1. block作为函数返回值时
2. 将block赋值给__strong指针时
3. block作为Cocoa API中方法名含有`usingBlock`的方法参数时
4. block作为`GCD API`的方法参数时
5. ....
.
.
.

基于以上结论:
MRC下block属性的建议写法

@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法

@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

但是为了统一,其实我们更建议不管是ARC环境还是MRC环境下block都用copy修饰

block使用了对象类型的auto变量的分析

因为上面我们分析的结果都是基于基本数据类型的,有些问题涉及不到,这里我们分析一种复杂的情况 , 即 block内部使用了对象类型的变量

首先我们定义一个Person对象

@interface Person : NSObject
@property (nonatomic,assign) int age;
@end

在该对象内部调用dealloc方法

#import "Person.h"
@implementation Person
-(void)dealloc{
    NSLog(@"Person -------dealloc");
}
@end
关于block--你想了解的几乎都在这里了_第13张图片

执行程序后 代码执行到20行的时候 调用了Person对象的dealloc方法 , 哥们 你在逗我么 这个地球人都知道好吧.....

不着急,我们一步步来 上面是预热

将我们的编译环境切换到MRC环境,执行下面的代码


关于block--你想了解的几乎都在这里了_第14张图片

断点停留在了25行,此时block还没有销毁,但是打印结果显示Person已经挂掉了

结论一:如果block是在栈上,将不会对auto变量产生强引用
同样的代码将编译环境切换到ARC,执行结果如下图:

关于block--你想了解的几乎都在这里了_第15张图片

ARC环境下,当代码执行到24行的时候person对象并没有销毁,说明block对象对person有一个强引用

如果我们将Person对象用__weak修饰呢?结果又是什么样子呢?如下图所示:

关于block--你想了解的几乎都在这里了_第16张图片

那么其底层是如何操作的呢?执行下面的命令将代码转化为C++代码:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
关于block--你想了解的几乎都在这里了_第17张图片

总结:当block内部访问了对象类型的auto变量时

如果block是__NSStackBlock__类型,将不会对auto变量产生强引用

如果block被拷贝到堆上,则会调用block内部的copy函数,copy函数内部会执行_Block_object_assign函数,该函数内部会根据auto变量的修饰符__strong 、__weak而做出相应的操作,如果是__strong 则形成强引用,而__weak 则会形成弱引用关系.

如果block从堆上移除,则会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,该函数会自动释放引用的auto变量,类似于release操作.

练习题:
仔细看下面的代码,Person对象会在什么时候销毁?


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    Person *person = [[Person alloc] init];
    person.age = 18;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"age is %d",person.age);
    });
    NSLog(@"Hello World");
}

分析过程:因为GCD中的block是在堆段中,所以block会一直存在直到执行完毕之后才会销毁,因为此处Person对象是一个__Strong,所以block会对Person对象形成一个强引用,所以Person对象会在3秒后Blcok执行完毕后释放
执行结果:

六. __block讲解

在具体讲解__block之前我们看一段代码:

关于block--你想了解的几乎都在这里了_第18张图片

我们想在block内部去修改age的值,但是此时系统编译失败并且提示我们缺少 __block修饰符,首先我们来思考下为什么在block内部不能去修改auto修饰的局部变量呢?

其实仔细看过上面的分析之后我们肯定知道答案了,因为作用域的原因,自动变量随时都有可能被销毁, 所以在block内部是不能去更改auto修饰的自动变量的.

除了系统给我们的提示, 添加__block修饰符外,其实我们还可以通过添加static修饰符,因为static修饰的局部变量是一个地址传递,所以完全没有问题.

或者将age放到函数外部,也就是将age设置成一个全局变量,这样也是可以更改的

但是不管是static修饰还是全局变量,age的性质就改变了 所以此处最好还是用__block修饰,那么__block内部做了什么事情呢?

老规矩..


关于block--你想了解的几乎都在这里了_第19张图片
关于block--你想了解的几乎都在这里了_第20张图片

我们通过C++代码可以看到 编译器会将__block变量包装成一个对象,然后将age的地址值传递给该对象,通过修改该对象内部的age来达到目的

注意:

__block可以用于解决block内部无法修改auto变量值的问题,但是不能修饰全局变量、静态变量(static)

__block的内存管理

当block在栈上时,仅仅是使用了__block变量,并没有对__block变量产生强引用

当block被copy到堆时
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign函数
_Block_object_assign函数会对__block变量产生强引用(仅当ARC时才有效,MRC时候并不会对__block变量产生强引用)

当block从堆中移除时
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose函数
_Block_object_dispose函数会自动释放引用的__block变量

Block的循环引用问题

两个对象相互持有,这样就会造成循环引用,如下图所示:

关于block--你想了解的几乎都在这里了_第21张图片

图中,对象A持有对象B,对象B持有对象A,相互持有,最终导致两个对象都不能释放。

列举个简单的例子:

Person.h

@interface Person : NSObject
/** 名字 */
@property (nonatomic, copy) NSString *name;

/** block */
@property (nonatomic, copy) void (^dosomethingBlock)();
@end

Person.m

@implementation Person

-(void)dealloc
{
     NSLog(@"%s", __func__);
}
@end

ViewController中去实例化Person对象:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *per = [[Person alloc] init];
    per.name = @"Jack";
    per.dosomethingBlock = ^{
        NSLog(@"-----------%@", per.name);
    };
}
@end

运行上面的代码,发现并没有调用 Person 对象中的 dealloc方法.说明此时发生了循环引用的问题,
通过上文这么长时间的分析,我相信大家一定知道block内部做了什么事情, 因为 Person 对象是用__strong修饰的,所以此时 __Block_Object_Assign方法内部会自动产生一个【强引用】指向【对象Person】

那么此时我们该如何解决这个问题呢? 通常的做法是创建一个 __weak 修饰的弱引用 指向 person对象,代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *per = [[Person alloc] init];
    
    __weak typeof(Person) *weakPerson = per;
    
    per.name = @"Jack";
    per.dosomethingBlock = ^{
        NSLog(@"-----------%@", weakPerson.name);
    };
}

运行上面的代码,发现 Person对象调用了 dealloc方法.

Block的嵌套

还是以上面的例子,我们在 per.dosomethingBlock中延迟三秒后打印 weakPerson.name,仔细查看下面的代码,你会发现什么问题?


- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *per = [[Person alloc] init];
    __weak typeof(Person) *weakPerson = per;
    per.name = @"Jack";
    per.dosomethingBlock = ^{
         NSLog(@"beign-------");
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(),
           ^{
               NSLog(@"after-----------%@", weakPerson.name);
           });
    
    };
    per.dosomethingBlock();
}

控制台输出结果:

2017-08-14 17:48:17.441 block[23041:1182337] beign-------
2017-08-14 17:48:17.441 block[23041:1182337] -[Person dealloc]
2017-08-14 17:48:20.729 block[23041:1182337] after-----------(null)

通过打印结果我们可以看到当 Block 被执行的时候立马打印了 beign-------信息,然后紧接着 Person对象被销毁, 3秒以后打印了 after-------信息,注意因为此时 Person对象已经被销毁了,所以打印出了 Null

所以 在延迟执行的Block内部 为了保住 Person对象不被销毁 我们需要使用一个强引用来保住 Person对象的命,稍微更改下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *per = [[Person alloc] init];
    __weak typeof(Person) *weakPerson = per;
    per.name = @"Jack";
    per.dosomethingBlock = ^{
         NSLog(@"beign-------");
        
        Person *strongPerson = weakPerson;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(),
           ^{
               NSLog(@"after-----------%@", strongPerson.name);
           });
    
    };
    per.dosomethingBlock();
}

控制台输出结果:

2017-08-14 17:50:04.947 block[23068:1184458] beign-------
2017-08-14 17:50:08.234 block[23068:1184458] after-----------Jack
2017-08-14 17:50:08.234 block[23068:1184458] -[Person dealloc]

通过上面的结果我们可以看到 在 dispatch_after中使用了 一个强引用 strongPerson来保住了 Person对象的命,所以此时才是我们想要的结果.

block的一些使用场景

将block作为对象属性,恰当时机的时候才去调用

场景一
比如我们从A控制器Modal到B控制器,在B控制器中点击了某个按钮后需要给A控制器传递一些数据,当然这种也可以使用代理来实现,但是我感觉 使用block更为简洁:

B控制器:

@interface BViewController : UIViewController

@property (nonatomic, strong) void(^buttonClickBlock)(NSString *param);

@end
-(void)buttonClick{
    if (_buttonClickBlock) {
        _buttonClickBlock(@"需要传递的参数");
    }
}

A控制器:

    BViewController *modalVc = [[BViewController alloc] init];
    modalVc.view.backgroundColor = [UIColor brownColor];
    
    modalVc.buttonClickBlock = ^(NSString *param) {
      
        NSLog(@"传递过来的参数:%@",param);
    };
    // 跳转
    [self presentViewController:modalVc animated:YES completion:nil];

场景二:
有一个UITableView,此时点击每一行都会做不同的事情,如果不用block 你是不是想着在点击的时候去判断行号,然后根据行号在去做对应的事情? 然而通过block我们可以简化这个流程:

在对应的实体模型中,我们只需要定义一个具体做事的block,例如:

@interface CellItem : NSObject

@property (nonatomic, strong) NSString *title;

// 保存每个cell做的事情
@property (nonatomic, strong) void(^doSomethingBlock)();

@end

然后在tableView中:点击的时候执行对应的代码块就可以了,是不是一下轻松了许多

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建模型
    CellItem *item1 = [CellItem itemWithTitle:@"打电话"];
    
    // 把要做的事情(代码)保存到模型
    item1.doSomethingBlock = ^{
        NSLog(@"打电话");
    };
    CellItem *item2 = [CellItem itemWithTitle:@"发短信"];
    item2.doSomethingBlock = ^{
        NSLog(@"发短信");
    };
    CellItem *item3 = [CellItem itemWithTitle:@"发邮件"];
    item3.doSomethingBlock = ^{
        NSLog(@"发邮件");
    };
    _items = @[item1,item2,item3];
}
// 点击cell的时候执行对应的block代码块就可以了
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    CellItem *item = self.items[indexPath.row];
    if (item.doSomethingBlock) {
        item.doSomethingBlock();
    }
}

将block当作函数参数来传递

当我们封装一个方法的时候,而这个方法具体要做什么事情不是方法内部能够决定的,但什么时候做是由内部决定的,(即内部决定执行时间,而外部传入具体做些什么)——这个时候就可以使用block来作为函数参数

列举一个经常使用的例子:

+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

对于系统提供的这个动画方法,要执行什么样的动画,执行完动画要做什么事情,该方法内部是不知道的,需要交给用户自己去实现

场景二:
要求封装一个计算器类CacultorManager,该类提供一个计算方法:怎么计算是由外界决定的,什么时候计算由内部决定.

CacultorManager.h

@interface CacultorManager : NSObject

/***保存计算的结果**/
@property (nonatomic, assign) NSInteger result;

- (void)cacultor:(NSInteger(^)(NSInteger result))cacultorBlock;

@end

CacultorManager.m

@implementation CacultorManager

- (void)cacultor:(NSInteger (^)(NSInteger))cacultorBlock
{
    
    if (cacultorBlock) {
      _result =  cacultorBlock(_result);
    }
}

@end

具体使用该类:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建计算器管理者
    CacultorManager *mgr = [[CacultorManager alloc] init];
    [mgr cacultor:^(NSInteger result){
        result += 5;
        result -= 6;
        return result;
    }];
    
    NSLog(@"%ld",mgr.result);
}
@end
将block当作函数返回值来传递

相信大家之前都写过类似这样的代码

[View mas_makeConstraints:^(MASConstraintMaker *make) {
      make.top.equalTo(anotherView);
      make.left.equalTo(anotherView);
      make.width.mas_equalTo(@60);
      make.height.mas_equalTo(@60);
}];

类似于 Masonry这种可以连续.top.equalTo(anotherView); 的写法我们称为链式编程,而实现这种思路的重点在于方法的返回值必须是block,而block必须返回本身,block的参数就是我们需要操作的数据

下面我们就来写一个类似这样的小功能:还是拿上面的计算器类CalculatorManager来举例子:

CalculatorManager.h

@interface CalculatorManager : NSObject

/***保存计算的结果**/
@property (nonatomic, assign) int result;

- (CalculatorManager *(^)(int))add;
- (CalculatorManager *(^)(int))sub;

@end

CalculatorManager.m

@implementation CalculatorManager

- (CalculatorManager *(^)(int))add
{
    return ^(int value){
        _result += value;
        
        return self;
    };
}

- (CalculatorManager *(^)(int))sub
{
    return ^(int value){
        _result -= value;
        
        return self;
    };
}

@end

在实现类中:

- (void)viewDidLoad {
    [super viewDidLoad];

    CalculatorManager *mgr = [[CalculatorManager alloc] init];
   
    mgr.add(5).add(5).sub(6).add(5);
    
    NSLog(@"%d",mgr.result);

}
@end

你可能感兴趣的:(关于block--你想了解的几乎都在这里了)