iOS block

落花浮王杯 提交于 2020-02-10 07:11:18

一.认识block

  Block作为C语言的扩展,并不是高新技术,和其他语言的闭包或lambda表达式是一回事。需要注意的是由于Objective-C在iOS中不支持GC机制,使用Block必须自己管理内存,而内存管理正是使用Block坑最多的地方,错误的内存管理 要么导致return cycle内存泄漏要么内存被提前释放导致crash。 Block的使用很像函数指针,不过与函数最大的不同是:Block可以访问函数以外、词法作用域以内的外部变量的值。换句话说,Block不仅 实现函数的功能,还能携带函数的执行环境。

 可以这样理解,Block其实包含两个部分内容

  1. Block执行的代码,这是在编译的时候已经生成好的;
  2. 一个包含Block执行时需要的所有外部变量值的数据结构。 Block将使用到的、作用域附近到的变量的值建立一份快照拷贝到栈上。

Block与函数另一个不同是,Block类似ObjC的对象,可以使用自动释放池管理内存(但Block并不完全等同于ObjC对象)

  先从一个简单的需求来说:传入两个数,并且计算这两个数的和,为此创建了这样一个block:

1
2
3
int (^sumOfNumbers)(int a, int b) = ^(int a, int b) {
    return a + b;
};

  这段代码等号左侧声明一个名为sumOfNumbers的代码块,名称前用^符号表示后面的字符串是block的名称。最左侧的int表示这个block的返回值,括号中间表示这个block的参数列表,这里接收两个int类型的参数。 而在等号右侧表示这个block的定义,其中返回值是可以省略的,编译器会根据上下文自动补充返回值类型。使用^符号衔接着一个参数列表,使用括号包起来,告诉编译器这是一个block,然后使用大括号将block的代码封装起来。

                                           

二.Block的类型与内存管理

根据Block在内存中的位置分为三种类型NSGlobalBlock,NSStackBlock, NSMallocBlock。

  • NSGlobalBlock:类似函数,未引用外部变量即为NSGlobalBlock,位于text段;
  • NSStackBlock:位于栈内存,函数返回后Block将无效;
  • NSMallocBlock:位于堆内存。

1、NSGlobalBlock如下,我们可以通过是否引用外部变量识别,未引用外部变量即为NSGlobalBlock,可以当做函数使用。

1

2

3

4

5

6

7

8

9

{

    //create a NSGlobalBlock

    float (^sum)(float, float) = ^(float a, float b){

 

        return a + b;

    };

 

    NSLog(@"block is %@", sum); //block is <__NSGlobalBlock__: 0x47d0>

}

2、NSStackBlock如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

{

    NSArray *testArr = @[@"1", @"2"];

 

    void (^TestBlock)(void) = ^{

 

        NSLog(@"testArr :%@", testArr);

    };

 

    NSLog(@"block is %@", ^{

 

        NSLog(@"test Arr :%@", testArr);

    });

    //block is <__NSStackBlock__: 0xbfffdac0>

    //打印可看出block是一个 NSStackBlock, 即在栈上, 当函数返回时block将无效

 

    NSLog(@"block is %@", TestBlock);

    //block is <__NSMallocBlock__: 0x75425a0>

    //上面这句在非arc中打印是 NSStackBlock, 但是在arc中就是NSMallocBlock

    //即在arc中默认会将block从栈复制到堆上,而在非arc中,则需要手动copy.

}

3、NSMallocBlock只需要对NSStackBlock进行copy操作就可以获取,但是retain操作就不行,会在下面说明

Blockcopyretainrelease操作 还是copy一段)

不同于NSObjec的copy、retain、release操作:

  • Block_copy与copy等效,Block_release与release等效;
  • 对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1;
  • NSGlobalBlock:retain、copy、release操作都无效
  • NSStackBlock:retain、release操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],(补:在arc中不用担心此问题,因为arc中会默认将实例化的block拷贝到堆上)在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之后生成新的NSMallocBlock类型对象。
  • NSMallocBlock支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain;
  • 尽量不要对Block使用retain操作。

 

三.捕获外界变量

  为了可以在block块中访问并修改外部变量,我们常会把变量声明成__block类型,通过上面的原理,可以发现,其实这个关键字只做了一件事,如果在block中访问没有添加__block这个关键字,会访问到block自己拷贝的那一份变量,它是在block创建的时候创建的,而访问加了__block关键字的变量,则会访问这个变量的地址所对应的变量。

  对于希望在block中修改的外界局部对象,我们可以给这些变量加上__block关键字修饰,这样就能在block中修改这些变量。下面两段代码等价,源代码:

1
2
3
4
5
6
7
8
9
CGPoint center = cell.center;
CGPoint startCenter = center;
startCenter.y += LXD_SCREEN_HEIGHT;
cell.center = startCenter;
[UIView animateWithDuration: 0.5 delay: 0.35 * indexPath.item usingSpringWithDamping: 0.6 initialSpringVelocity: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
    cell.center = center;
} completion: ^(BOOL finished) {
    NSLog("animation %@ finished", finished? @"is", @"isn't");
}];

使用block后:

1
2
3
4
5
6
CGPoint center = CGPointZero;
    CGPoint (^pointAddHandler)(CGPoint addPoint) = ^(CGPoint addPoint) {
    return CGPointMake(center.x + addPoint.x, center.y + addPoint.y);
}
center = CGPointMake(100, 100);
NSLog(@"%@", pointAddHandler(CGPointMake(10, 10)));    //输出{10,10}

  block在捕获变量的时候只会保存变量被捕获时的状态(对象变量除外),之后即便变量再次改变,block中的值也不会发生改变。

四.循环引用

  block在iOS开发中被视作对象,因此其生命周期会一直等到持有者的生命周期结束了才会结束。另一方面,由于block捕获变量的机制,使得持有block的对象也可能被block持有,从而形成循环引用,导致两者都不能被释放:

1
2
3
4
5
6
7
8
9
10
11
12
@implementation LXDObject
{
    void (^_cycleReferenceBlock)(void);
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    _cycleReferenceBlock = ^{
        NSLog(@"%@", self);   //引发循环引用
    };
}
@end

遇到这种代码编译器只会告诉你存在警告,很多时候我们都是忽略警告的,这最后会导致内存泄露,两者都无法释放。跟普通变量存在__block关键字一样的,系统提供给我们__weak的关键字用来修饰对象变量,声明这是一个弱引用的对象,从而解决了循环引用的问题:

1
2
3
4
__weak typeof(*&self) weakSelf = self;
_cycleReferenceBlock = ^{
    NSLog(@"%@", weakSelf);   //弱指针引用,不会造成循环引用
};

五.关于引用计数

  在block中访问的对象,会默认retain.而添加了__block的对象不会被retain.

六.block用于传值

  使用Block的地方很多,其中传值只是其中的一小部分,下面介绍Block在两个界面之间的传值:

       先说一下思想:

       首先,创建两个视图控制器,在第一个视图控制器中创建一个UILabel和一个UIButton,其中UILabel是为了显示第二个视图控制器传过来的字符串,UIButton是为了push到第二个界面。

        第二个界面的只有一个UITextField,是为了输入文字,当输入文字,并且返回第一个界面的时候,当第二个视图将要消失的时候,就将第二个界面上TextFiled中的文字传给第一个界面,并且显示在UILabel上。

下面是主要代码:(因为我是用storyBoard创建的工程,所以上面的属性和相应的方法,是使用系统生成的outlet)

1、在第二个视图控制器的.h文件中定义声明Block属性

typedef void (^ReturnTextBlock)(NSString *showText);

@interface TextFieldViewController : UIViewController

@property (nonatomic, copy) ReturnTextBlock returnTextBlock;

- (void)returnText:(ReturnTextBlock)block;

@end

        第一行代码是为要声明的Block重新定义了一个名字

    ReturnTextBlock

        这样,下面在使用的时候就会很方便。

        第三行是定义的一个Block属性

        第四行是一个在第一个界面传进来一个Block语句块的函数,不用也可以,不过加上会减少代码的书写量

2、实现第二个视图控制器的方法

- (void)returnText:(ReturnTextBlock)block {
  self.returnTextBlock = block;
}
- (void)viewWillDisappear:(BOOL)animated {
  
  if (self.returnTextBlock != nil) {
    self.returnTextBlock(self.inputTF.text);
  }
}

        其中inputTF是视图中的UITextField。

        第一个方法就是定义的那个方法,把传进来的Block语句块保存到本类的实例变量returnTextBlock(.h中定义的属性)中,然后寻找一个时机调用,而这个时机就是上面说到的,当视图将要消失的时候,需要重写:

- (void)viewWillDisappear:(BOOL)animated;

方法。

3、在第一个视图中获得第二个视图控制器,并且用第二个视图控制器来调用定义的属性

如下方法中书写:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
  // Get the new view controller using [segue destinationViewController].
  // Pass the selected object to the new view controller.
  TextFieldViewController *tfVC = segue.destinationViewController;
  
  [tfVC returnText:^(NSString *showText) {
    self.showLabel.text = showText;
  }];
}

     可以看到代码中的注释,系统告诉我们可以用[segue destinationViewController]来获得新的视图控制器,也就是我们说的第二个视图控制器。

        这时候上面(第一步中)定义的那个方法起作用了,如果你写一个[tfVC return Text按回车  ,系统会自动提示出来一个: 

tfVC returnText:<#^(NSString *showText)block#>

的东西,我们只要在焦点上回车,就可以快速创建一个代码块了,大家可以试试。这在写代码的时候是非常方便的。

 

 

六.总结

  block捕获变量、代码传递、代码内联等特性赋予了它多于代理机制的功能和灵活性,尽管它也存在循环引用、不易调试追溯等缺陷,但无可置疑它的优点深受码农们的喜爱。如何更加灵活的使用block需要我们对它不断的使用、探究了解才能完成。

  

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!