iOS-block(一)-初探

基本概念

什么是block?《Objective-C高级编程》这本书里是这样定义的:

带有自动变量(局部变量)的匿名函数。顾名思义,匿名函数就是没有名称的函数。也被称为闭包(closure)或者Anonymous function

我们可以理解为block就是一个没有名称的函数。定义block的方式和定义函数的方式是相似的,而block还可以作为参数使用。当block被调用其块内的代码才会被执行。

定义

根据block的定义,我们可以知道,block的主要组成是返回值和参数。其表达式如下:

^+返回值类型+参数列表+表达式

按照是否存在返回值和参数,我们可以将block的定义分为以下几种:

    1. 无返回值+无参数
void(^myBlock)(void) = ^void(void) {
    
};
// 可以简写成:
void(^myBlock)(void) = ^ {
    
};
    1. 无返回值+有参数
void(^myBlock)(int a) = ^void(int num) {
    
};
// 可以简写成:
void(^myBlock)(int a) = ^(int num) {
    
};
    1. 有返回值+无参数
int(^myBlock)(void) = ^int(void) {
    return 10;
};
// 可以简写成:
int(^myBlock)(void) = ^int {
    return 10;
};
    1. 有返回值+有参数
int(^myBlock)(int a, int b) = ^int(int a, int b) {
    return a + b;
};

当参数和返回值为void可以忽略。

在实际开发中,我们经常使用typedef来定义block:

typedef int(^myBlock)(int a, int b);
myBlock mb = ^int(int a, int b) {
        return a + b;
};
NSLog(@"==myBlock==%d==", mb(1, 2));

block与外界变量的关联

下面我们来看一个例子:

typedef int(^myBlock)(int a, int b);

int d = 10;
myBlock mb = ^int(int a, int b) {
    return a + b + d;
};
d = 5;
NSLog(@"==myBlock==%d==", mb(1, 2));

我们在mb内部使用外部变量int d,随后又给d重新赋值,此时调用mb(1, 2)block内部d的取值是5还是10呢?运行程序,控制台输出13。此例子说明了block具有截获外部变量的能力,而且截获之后,变量在block的值是固定的,不会随着外部的改变而改变。

我们接着在其外部定义一个变量e:

int e = 0;

当我们在block内部对e进行赋值操作的时候,编译器会提示错误:

Variable is not assignable (missing __block type specifier)

意为变量不可以被分配使用,是因为缺少__block修饰符。那么我们为上述变量de的声明加上__block修饰符,我们立刻看到编辑器没有错误了。运行程序,此时控制台输出8,这说明此时block中的取值不再是10而是5了。它不但被我们分配使用了,可以随着外界的改变而改变了,甚至我们可以随意的在block内部修改d的值了,这到底是为什么呢?

从表面上看,没有被__block修饰的变量,我们在block内部使用的时候,只是截获其当时的值,所以其不会再改变,而被__block修饰的变量,我们截获该变量的地址,所以它不论怎们改变,我们都能截获到。

block分类

iOS中,我们依据内存情况将block分为6中

  • _NSConcreteGlobalBlock:全局block,不访问外界变量(包括堆中和栈中的变量)
void (^block)(void) = ^{
    NSLog(@"==block==");
};
  • _NSConcreteMallocBlock:堆block,存在于堆内存中,是带一个引用计数的对象,需要自己进行内存管理。变量本身在栈中,因为block能够自动截获变量,为了访问到变量,会将变量从堆内存中copy成栈内存中。
int a = 10;
void (^block)(void) = ^{
    NSLog(@"==a==%d==", a);
};
  • _NSConcreteStackBlock:栈block,存于栈内存中,超出其作用域则马上进行销毁。作为方法或者函数的参数的时候不会被copy到堆上。
NSLog(@"%@",^{
    NSLog(@"==block==");
});
  • _NSConcreteAutoBlock
  • _NSConcreteFinalizingBlock
  • _NSConcreteWeakBlockVariable

前3种在日常开发中是很常见的,后3种是系统级别的block,一般比较少用。

我们知道程序在编译的时候内存的分布有:堆区(heap)、栈区(stack)、文字常量区、程序代码区、全局区/静态区。

image

这张图也就解释了为什么我们使用block作为属性的时候修饰符都是用copy_NSConcreteGlobalBlock是全局block,它只能存在于一个函数的内部,并不能作为属性;而_NSConcreteStackBlock是栈block,存于栈内存中,超出其作用域则马上进行销毁,当我们使用copy就会将其copy到堆内存中,这样会延长block的生命周期,防止出现异常;而_NSConcreteMallocBlock本身就是堆block,使用copy也不会对其有影响。

block的使用

block既然是一个匿名函数,那么它就可以作为函数使用,当然,它也可以作为作为函数的参数、或者函数的返回值调用。

上面的例子大多数都是block作为函数使用,我们也就不再赘述了。下面我们就看看其他两种情况:

首先我们先给block设置一个别名,这样看着简单明了一些。

typedef int(^SumBlock)(int a, int b);
  1. block作为函数的参数
- (void)blockAsParameter:(SumBlock)mb {
    NSLog(@"==blockAsParameter实现==%@==", mb);
    mb(1, 2);
}

可以调用一下,然后运行程序:

[self blockAsParameter:^int(int a, int b) {
        NSLog(@"==blockAsParameter调用==%d==", a + b);
        return a + b;
}];

// 控制台输出
==blockAsParameter实现==<__NSGlobalBlock__: 0x106a4d090>==
==blockAsParameter调用==3==
  1. block作为函数的返回值
- (SumBlock)blockAsReturns {
    NSLog(@"==blockAsReturns==");
    return ^int(int a, int b) {
        return a + b;
    };
}

SumBlock mb2 = [self blockAsReturns];
NSLog(@"==blockAsReturns==%d==", mb2(1, 2));

==blockAsReturns==
==blockAsReturns==3==

其实说的通俗点,block就是封装一段代码块,这段代码块可以像变量一样被使用。

block的循环引用问题

我们在日常使用block的时候一定要注意一个问题,那就是循环引用。因为block经常是作为变量被self持有,或者是block的持有者被self作为变量持有,然而当我们在block内部使用self的时候就会造成循环引用。我们来看一个例子:

@property (nonatomic, assign) int num1;
@property (nonatomic, assign) int num2;
@property (nonatomic, copy) SumBlock propertyBlock;

self.num1 = 1;
self.num2 = 2;
self.propertyBlock = ^int(int a, int b) {
    NSLog(@"==a==%d==b==%d==", self.num1, self.num2);
    return a + b;
};

编译器会直接提示:

Capturing 'self' strongly in this block is likely to lead to a retain cycle

意为在block中强引用self可能会造成循环引用。上述例子中,num1num2propertyBlock作为属性被self持有,而我们又在block里使用了num1num2,这就相当propertyBlock又持有了self,这就造成了循环引用。强持有会对引用计数进行处理,循环引用会导致对象无法被释放,就影响了对象的引用计数,会造成内存问题。那么这个问题如何解决呢?

__weak typeof(self) weakSelf = self;
self.propertyBlock = ^int(int a, int b) {
    NSLog(@"==a==%d==b==%d==", weakSelf.num1, weakSelf.num2);
    return a + b;
};

这样就解决了循环引用问题。那么weakSelf是如何解决循环引用的呢?由于现在引入了weakSelf,持有的情况就变成了weakSelf持有selfself持有block,而block又持有weakSelf,但是需要注意的是weakSelf持有self是弱引用,只是一个指向,引用计数并没有发生改变,所以就打破了循环引用。

但是使用__weak需要注意一点,就是对象的释放时间。

self.propertyBlock = ^int(int a, int b) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"==a==%d==b==%d==", weakSelf.num1, weakSelf.num2);
    });
}

如果我们进入页面之后,立即退出页面,控制台输出:

==a==0==b==0==

这说明,dealloc之后持有的对象却是已经被释放了。如果我们想等到打印结果输出之后,再进行dealloc该怎么处理。

  1. 使用__strong

我们可以在block里面使用__strong再将weakSelf转化为强引用即可。此时虽然strongSelf为强引用,但是只是在block作用域内的,当block内任务执行完毕,自然也会释放,和外界并没有任何关系。

__strong typeof(weakSelf) strongSelf = weakSelf;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"==num==%d==", strongSelf.num);
});
  1. self作为参数传入block

blcok具有截获变量的能力,当参数传入blockblockcopy一份使用,此时copy出来的变量和原来的就没有关系。这样也打破了循环引用。

typedef int(^MinusBlock)(int a, int b, ViewController *vController);

self.num = 10;
self.mb = ^int(int a, int b, ViewController *vController) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"==num==%d==", vController.num);
    });
    return a - b;
};
self.mb(2, 1, self);

block的基础就介绍到这里,下一章我们再来看看block的底层分析。

参考文献:
Objective-C高级编程 iOSOS X多线程和内存管理》

你可能感兴趣的:(iOS-block(一)-初探)