带你深入理解IOS Block

Block

在Objc中,GCC编译器的更新引入了Block语法,这为objc语言提供了良好的闭包的功能,并为 Mac OS 与 iOS 的多个系统API所使用。
它的基本语法如下

//语法形式
return_type (^block_name)(parameters)
//实现
int addtional = 5;
int (^addBlock)(int a,int b) = ^(int a, int b){
    return a + b + addtional;
}

当声明与实现一个Block时,创建的闭包会捕获在它的域中的任何涉及的变量,通过在内存中持有他们,能够在block的实现中对其进行访问。在默认情况下,任何在block的域中被捕获的变量都不能被修改,除非这个变量已被给予了__block的标志。如上面的代码中,如果我们在block中修改addtionnal的值,编译器就会报错。我们需要做出以下修改,才能使addtionnal变量能在blo这为objc语言提供了良好的闭包的功能 ck中修改。

__block int addtional = 5;

当block捕获了一个对象时,它会对其进行retain操作,并在block代码执行完毕完release对象,这样才能保证在block执行过程中,对象不会因引用计数为0而被释放掉。我们需要理解的是,block本身就是一个对象,它对其他对象的引用与一般的对象引用类似,都是需要对引用对象进行retainrelease

Block的实现

我们所需要知道的是 block 就是一个对象,在它所在的内存中,保存着block自身的实现函数,可在调用block时用block自身的代码替代,同时保持着一个Block描述,标志着block的内存size与持有对象的指针。

Block的类型

stack block

看看下面这段代码,当block被定义时,block会被分配在stack(栈)中的一块内存中,这意味着这个block仅在自己所声明的域中生效,因此,这份代码是会出错的

void (^block)();
if(/*true*/)
{   int m=1;
    block = ^{ m++;
        NSLog(@"AAAA");
    };
}
else
{   int m=2;
    block = ^{ m--;
        NSLog(@"BBBB");
    };
}
block();

因为声明的block只在所属的域中生效,因为调用block()时,定义的两个block实现已经失效了,内存已经被释放了,在stack中退出,这就是 stack block.ARC下默认会被copy到堆上,MRC下默认是栈

heap block

为了解决这个问题,我们可以通过copy将block由stack copy至 heap,这样block就能够在它所属域之外被引用。当block保存在stack中时,系统机制会在调用完毕后自动清理它,相比之下,当在heap中时,block就与其他变量类似,接受引用计数管理,当block没必有再进行持有时,需要对其进行release操作(在ARC中,会自动插入release代码),没有对象持有它时,就会对其heap中的内存进行释放。如以下代码

 void (^block)();
if(/*true*/)
{
    block = [^{
        NSLog(@"AAAA");
    } copy];
}
else
{
    block = [^{
        NSLog(@"BBBB");
    } copy];
}
block();

这样的话这段代码是正确的,当然如果在非ARC环境下,需要对block执行release操作。

grobal block

全局 blcok与之前的stack block 、 heap block 不同,当一个block在闭包中不捕获程序的任何上下文(如各种程序中的变量)时,编译器在编译阶段就能够知道这个block执行的所需要的所有信息,这时,block会当做全局变量保存在全局内存中,以相当于单例的形式存在,它将不会收到任何release消息。这是编译器的一个优化点,减少了当block被copy或销毁时的多余操作。

用typedef来定义block类型

之前提及了block的声明语法

return_type (^block_name)(parameters)

我们可以看到,在block的声明中,我们规定了block的输入参数,以及返回的类型。这种声明的语法与objc其他对象(或函数)的声明语法有着很大的区别,这大大增加了我们对这种语法的记忆与理解的难度,特别是需要用block作为参数来声明与定义类的函数。为了定制更好的API外部接口,我们可以对经常使用的block的声明做类型定义

typedef int(^SomeBlock)(BOOL flag,int value);

这样的话,我们在类型系统中添加了一种新的类型SomeBlock,当我们需要使用这个block类型时,我们只需要简单使用这个新类型

SomeBlock block = ^(BOOL flag,int value){
    //do something
}

这样子,代码会变得更简洁,更有可读性,就如同我们在声明一个普通的objc对象一样。

使用Block减少代码分离

在Block语法出现之前,我们使用delegate委托方式,通过异步任务实现与委托方法的实现分离,处理异步任务完成后的回调工作。然而,当一个类作为一个相同的委托类成为了多个异步任务的委托时,我们就需要在这个类的委托方法实现中区分开来,并在同一个委托方法的实现中对不同异步任务的回调进行不同的处理,这样会使委托函数十分冗长,可读性变差,如下

- (void)tableView:(UITableView*)tableView didSelectedAtIndexPath:(NSIndexPath*)indexPath
{
    if(tableView == _tableViewA)
    {
        //do something
    }
    else if (tableView == _tableViewB)
    {
        //do otherThing
    }
}

这样子不仅我们需要在同一个函数做不同的处理,而且我们需要持有多个异步任务执行者的指针,在委托方法一一比对来区分调用委托者。而在block语法中,我们无需区分调用者,我们在声明调用者时,也声明了它回调的委托处理方法

//假设tableView支持block语法
UITableView *tableViewA = [[UITableView alloc] init];
[tableViewA startWorkWithSelectionHandler:^(NSIndexPath *indexPath){
    //do something with indexPath
}];

UITableView *tableViewB = [[UITableView alloc] init];
[tableViewB startWorkWithSelectionHandler:^(NSIndexPath *indexPath){
    //do something with indexPath
}];

这样使业务处理代码在一处一起实现,代码更具备可读性。

避免Block 的 retain cycle

在Block的使用中,我们如果不考虑仔细,就非常容易引人retain cycle引用循环的问题,如以下代码

//执行类
typedef void(^IFNetWortFetcherCompletionHandler)(NSData *data);
- (void)startWithCompletionHandler:(IFNetWortFetcherCompletionHandler)completion
{
    self.completion = completion;
    // 开始请求,当请求完成后,我们调用completion委托block
}

//委托类
- (void)download
{
    _networkFetcher = [[IFNetWortFetcher alloc] initWithUrl:_url];
    [[_networkFetcher startWithCompletionHandler:^(NSData *data)
    {
        _fetcherData = data;
    }];
}

这是一个很常见的retain cycle,在委托类中,我们持有了 networkFetcher ,而在 networkFetcher中, 我们持有了block,而在block中,我们通过捕获了_fetcherData,持有了委托类的self对象,这就造成了引用循环。
这个retain cycle能够通过打断任意一条链条来解决,而不是简单的一句__block __weak IFNetWortFetcher *wself = self能够解决的。

在委托类中,我们可以选择不持有 networkFetcher ,来打破这个链条,这在普遍的网络库中实行,在编写请求数据的代码中,一般不建议持有数据请求器

- (void)download
{
    IFNetWortFetcher *networkFetcher = [[IFNetWortFetcher alloc] initWithUrl:_url];
    [[networkFetcher startWithCompletionHandler:^(NSData *data)
    {
        _fetcherData = data;
    }];
}

当然,我们不能将打破引用循环的任务主观地交给调用者,我们需要在内部解决这个问题,当执行类对象异步任务完成后,我们要主动去释放持有的block

- (void)startWithCompletionHandler:(IFNetWortFetcherCompletionHandler)completion
{
    self.completion = completion;
    // 开始请求,当请求完成后,我们调用completion委托block
    self.completion = nil;
}

1Block是什么?
 - 匿名函数
 - 截获自动变量
2Block语法。
3Block类型变量。
4Block的用途。
 - 作为函数参数
 - 反向传值
 - 循环引用
(delegate差不多作用,但是显得更加简洁)

首先就是Block是什么?用一句话来概括就是带有自动变量的匿名函数。
那么我们解释清楚了什么是“匿名函数”,什么是“自动变量”,那么相信大家大概就对Block有了一个大概的认识。

  • 匿名函数
    匿名函数顾名思义就是不带名字的函数,在C语言中不允许这样的方法存在,而在OC中的Block则可以用指针来直接调用一个函数,但虽说如此我们还是需要知道指针的名称。(关于这点,额~~我们还是不要纠结的比较好。~!~~)
  • 自动变量
    自动变量
    在Block中的具体表现就是截获自动变量,来看下面这一段代码:
 int b = 0;
    void (^blo)() = ^{
        NSLog(@"Input:b=%d",b);
    };
    b = 3;
    blo();
    /**
     *    Input:b=0
     */

虽然我们在调用blo之前改变了b的值,但是输出的还是Block编译时候b的值,所以截获瞬间自动变量就是:在Block中会保存变量的值,而不会随变量的值的改变而改变。

我们再来看一段代码

int b = 0;
    void (^blo)() = ^{
        b = 3;
    };

这段代码编译出错,编译器提示的大概就是不能在Block中改变变量的值。因为在Block中截获了变量的瞬间值以后就不能再改变变量的值,如果想要在Block中改变变量的值,那么我们只需要在变量声明的时候加上__Block修饰符,像这样:

__block int b = 0;
    void (^blo)() = ^{
        b = 3;
    };

然而这样的情况又是允许的:

 NSMutableArray *array = [[NSMutableArray alloc]init];
    void (^blo)() = ^{
        [array addObject:@"Obj"];
    };

为什么呢,因为我们只是对截获的变量进行了操作,而没有进行赋值,所以对于截获变量,可以进行操作而不可以进行赋值。

还有一点需要注意,在Block中不可以对C语言数组进行操作,原因是:~~~不支持。。。。

结合匿名函数和截获自动变量的特性,Block可以做很多事情,我们下面在看。


我们来具体看一下Block语法的书写,我们首先来看一个完整的Block:

 ^ NSString *(NSString *a,NSString *b){
        return a;
    };

我们来分别解释下每一个部分都是什么东西:

  • “^”这个符号表示这是一个Block;
  • NSString *表示返回值。
  • (NSString a,NSString b)这个括号中是Block的参数,语法和C语言类似。

其实我们可以省略Block的返回值,像这样写:

^ (NSString *a,NSString *b){
        return a;
    };

这样写和上面那种写法是一模一样的,其实如果没有参数列表我们甚至可以省略参数列表,像这样:

^ {
        NSLog(@"我没有参数列表");
    };

如果把这段代码写完整,那么就是这样的:

^void(void) {
        NSLog(@"我没有参数列表");
    };

为什么需要Block变量?我们可以这样理解,我们通过这个Block变量来获取Block的指针,然后通过这个指针就可以来使用Block函数。我们先来看一下如何声明一个Block变量

      int (^Blo)(NSString *s1,NSString *s2);

对照前面的Block函数,我们可以比较容易的理解各个部分的含义:

他们分别是:

  • 返回值
  • 变量名
  • 参数列表

好的,然后我们用上面讲到的Block语法来对这个Block变量进行赋值:

    int (^Blo)(NSString *s1,NSString *s2);

    Blo = ^(NSString *s1,NSString *s2){
        return 1;
    };

然后我们就可以将这个Block变量当作C语言函数来使用了。


那么Block到底怎么用呢?

Block能够当作函数参数,首先我们声明一个Block类型变量 ,并加上typedef修饰符,像这样:

typedef void(^Blo)(NSString *s1,UIColor *c);

这样我们就可以使用Blo来表示这个Block,然后我就可以将Blo加入到函数参数中,我们来声明一个函数:

-(void)func:(Blo)BlockPra{

    BlockPra(@"Str",[UIColor redColor]);

}

然后我们可以这样使用这个函数:

[self func:^(NSString *s1, UIColor *c) {
        NSLog(@"%@",s1);
        self.view.backgroundColor = c;
    }];

是不是觉得十分眼熟,平时使用的许多回调当中大多都是这样的形式,可能其中其较多的就是网络回调了,我们只需要调用方法,然后在回调当中就可以对结果进行操作,很多苹果自己写的API都是使用了这样的方法,这样做的好处就是形式上十份简洁,当然像这种地方你使用delegate肯定也是可以的,但是表现上就没有Block那么简洁,使用起来也没有Block那么方便。

除此之外,Block还可以用来作为控制器之间的一个通信。
前面我们已经知道Blcok是一个匿名函数,同时也是一个指针,那么使用Block就可以弥补在iOS中函数传递的功能。通常是这么用的:

页面B的.h文件中定义了这样一个Block执政,然后声明了一个变量,像这样:

  typedef void(^Blo)(NSString *s1,UIColor *c);
  @property (nonatomic, copy) Blo block;

然后我们在页面A当中有这么一段代码:

ViewController *b = [[ViewController alloc]init];

    __weak  ViewController *wself = self;
    b.block = ^(NSString *s1,UIColor *c){
        NSLog(@"%@",s1);
        wself.view.backgroundColor = c;
    };
    [self.navigationController pushViewController:b animated:true];

然后在页面B的任意地方我们调用block变量,像这样:

     self.block(@"str",[UIColor redColor]);

都会在A页面中调用B页面传过来的参数,在A页面进行操作,对控制器A进行改变,这样的做法通常用做 控制器 反向传值。

在这里有一点需要注意就是Block的使用引起的循环引用。如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆时,改对象为Block所有。这样容易引起循环引用,从而发生内存泄漏,然而我们只需要保证当前控制器也就是self在需要释放的时候正确释放就可以,所以我们再来看上面那段代码:

      __weak  ViewController *wself = self;

我们定义一个wself变量并加上__weak修饰符,在Block代码块中,所有需要self的地方都用wself来替代。这样就不会增加引用计数,所以Block持有self对象也就不会造成循环引用,从而造成内存泄漏。
不管是将Block当作函数参数,还是用来反向传值,其实都是对Block的本质,也就是“带有自动变量的匿名函数”的两个修饰,“带有自动变量”、“匿名函数”这两个特性 的应用。

如果你想挑战自己对Block的认识请点我

参考文章

objc中的Block原理实现与注意事项

iOS-Block的使用你看我啊

如果你想深入了解,请点击查看唐大师的博客


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值