iOS Developer内功修炼iOS相关技术

学会使用Objective-C中的block

2016-09-03  本文已影响4785人  HK_Hank

Apple从OS X 10.4和iOS 4以后开始支持block,相对于delegate,block有很多便捷之处,使得代码更简洁,可读性更强。但是如果使用不当,则会造成很多问题。本文结合自己的经验和《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》书中的知识点,介绍block的相关知识点。

block语法

我们通过以下图来了解block的语法,图片来自这里

block语法结构图

我们来看看上面的图,代码如下

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

根据图中的解释,我们从左向右来看,该block返回值为int类型,'^'符号声明一个名为myblock的block,该block有一个int类型的入参,等号右边则为block的定义,block有一个名为num的int类型的入参,{return num * multiplier;};则为该block的block实现部分。

该block的调用方法如下,看起来像C的函数调用。

int result =  myBlock(2); //reslut = 14;

我们来看看复杂一点的情况:

- (void)startWithBlock:(void(^)())block {
    block();
}

- (void)testBlock {
    NSString *strBlock = @"NSStackBlock";
    [self startWithBlock:^{
        NSLog(@"%@",strBlock);
    }];
}

控制台输出

NSStackBlock

以上代码,新手看起来可能是会有些费劲的。我们一步一步来,首先,我们调用testBlock函数,在该函数中,

^{
     NSLog(@"%@",strBlock);
 }];

该代码块实际上是传给了startWithBlock函数的参数block,当执行startWithBlock函数时,调用block(),实际上就是执行了以上代码块。

使用typedef定义block类型
以上代码可以通过typedef来定义block,以便阅读,如下

typedef int (^myBlock)(int num);

在定义某个block类型时,可以使用

myBlock aBlock = ^(int num) {
    //Implemention
};   

这样看起来,要比之前简单得多。

block捕获外部变量

block内可以访问block之前定义的变量:

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};
int result =  myBlock(2); //reslut = 14;

但是,如果想在block内部改变multiplier的值,编辑器则会报错

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};

编辑器会提示: 变量不能被赋值,需要加上__block修饰符

error: variable is not assignable (missing __block type specifier)

此时,需要将该变量使用__block修饰:

__block int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};

如果multiplier变量是static、static global或者global变量,则不需要添加__block,该值也是可以在block内部修改的。

static int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};

因为static、static global或者global变量都是存储在内存中的全局区(静态区),对于这三种类型变量,block内部是捕获了其指针,则可以直接访问修改;而对于之前的临时变量,block则只是捕获了该变量的值,无法修改到外部的变量。

block内部还可以访问类的实例变量和self变量

@interface EOCClass : NSObject 
@property (nonatomic, copy) NSString *anInstanceVariable;
@end

@implementation EOCClass

- (void)anInstanceMethod {
    
    void (^someBlock)() = ^ {
        self.anInstanceVariable = @"Something";
    };
    someBlock();
    NSLog(@"self.aninstanceVaraible = %@", self.anInstanceVariable);
    //self.aninstanceVaraible = Something
}

@end```

####block的内部结构
block 的数据结构定义如下(图片来自 [这里](http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/)):
![block内存布局](http:https://img.haomeiwen.com/i809937/6d6f7c759832c3a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
对应的结构体定义如下:

struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void src);
void (
dispose)(void *);
};

struct Block_layout {
void isa;
int flags;
int reserved;
void (
invoke)(void *, ...);
struct Block_descriptor descriptor;
/
Imported variables. */
};

从上面代码看出,一个 block 实例实际上由 6 部分构成:

* **isa指针**:指向该block类型的类的指针,
每个Objective-C对象,都有一个`isa`指针,指向对象的类,而Class里也有个`isa`的指针, 指向meteClass(元类)。元类保存了类方法的列表。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身。如下图
![图片来自《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》](http:https://img.haomeiwen.com/i809937/d400b23575e007d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
对于block,isa指针可以指向
`_NSConcreteStackBlock`、`_NSConcreteMallocBlock`、`_NSConcreteGlobalBlock`这三种类型

* **flags**:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
* **reserved**:保留变量,我的理解是表示block内部的变量数。
* **invoke**:函数指针,指向block的实现代码地址。
* **descriptor**:指向结构体的指针,block的附加描述信息,比如保留变量数、block的大小、copy和dispose辅助函数的函数指针指针
*copy函数为当block执行copy操作或者当block从栈上拷贝到堆上时调用,dispose函数则是block在堆上释放时调用*。
* **variables**:block内部捕获的对象,如
```objective-c
void (^blk)(void) = ^{print(fmt,val)};

此时,variables中则为fmt和val这两个变量

block的类型

block有_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock这三种类型。
三种block在内存中存储位置如下图

图片来自《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》
block类型的区分

以下情况,block为_NSConcreteGlobalBlock类型

除了以上两种情况,其他的block为_NSConcreteStackBlock类型。
而对于_NSConcreteMallocBlock,只有当_NSConcreteStackBlock类型的block执行copy操作(手动或者系统执行)时,该block才会是_NSConcreteMallocBlock类型

我们来看看代码,直观的看看这三种类型的block

  1. _NSConcreteGlobalBlock
    由类型名字可以得知,该block是存储在在内存中全局区的。
void (^block)() = ^{
    NSLog(@"This is a global block");
};
NSLog(@"%@",block);

控制台输出

<__NSGlobalBlock__: 0x104fd52d0>

或者

int globalVal = 1; //此处为全局变量
int (^myBlock)(int) = ^(int num) {
    return num * globalVal;
};
NSLog(@"%@",block);

控制台输出

<__NSGlobalBlock__: 0x104fd5310>

该block所需要的全部信息都能在编译期确定。该block是全局存在的,相当于单例了。

  1. _NSConcreteStackBlock
    由该类型的名字可以看出,该block所占的内存区域是分配在栈(stack)中的。也就是说,块只在定义它的那个范围内(作用域)内有效。如下面代码:
int multiplier = 7;
NSLog(@"%@",^(int num) {
    return num * multiplier;
};);

控制台输出

<__NSStackBlock__: 0x7fff59615a18>

以上代码,block内部捕获了multiplier这个外部的局部变量,所以是_NSConcreteStackBlock类型。
因为该block存在在栈上,在超过block的作用域时,该block就会被系统释放,就有可能会出现block内部的代码还没有走完,就被释放掉的情况。对于这种情况,应该对block执行copy操作,将block复制到堆上。
注:在ARC下,系统在大部分情况下,会将block从栈上复制到堆上,这个后面会细说

  1. _NSConcreteMallocBlock
    对以上代码中的block执行copy操作,block就变成了_NSConcreteMallocBlock类型,如下
int multiplier = 7;
NSLog(@"mallocBlock:%@",[^(int num) {
    return num * multiplier;
} copy]);

控制台输出

<__NSMallocBlock__: 0x6000000486a0>

拷贝到堆后,block的生命周期就与一般的OC对象一样了。

ARC 下 block 的自动拷贝和手动拷贝

ARC下,以下几种情况,系统会将block从栈上自动复制到堆上

因为在ARC下,对象默认是用__strong修饰的,所以大部分情况下编译器都会将 block从栈自动复制到堆上,除了以下情况

看看代码
block作为函数的返回值,如下
- (void(^)())blockReturn {
    NSString *strBlock = @"NSMallocBlock";
    return ^(){
        NSLog(@"%@",strBlock);
    };
}
NSLog(@"%@",[self blockReturn]);

控制台输出

<__NSMallocBlock__: 0x7fa161f081f0>
block赋值给强引用block
typedef void(^block)();

NSString *strBlock = @"NSMallocBlock";
block mallocBlock = ^(){
    NSLog(@"%@",strBlock);
};
NSLog(@"%@",mallocBlock);

控制台输出

<__NSMallocBlock__: 0x7fedd0d26110>
将block作为临时变量
NSString *strBlock = @"NSStackBlock";
NSLog(@"%@",^(){
    NSLog(@"%@",strBlock);
});    

控制台输出

<__NSStackBlock__: 0x7fff563aa9b0>
block作为函数参数
- (void)startWithBlock:(void(^)())block {
    NSLog(@"%@",block);
}

- (void)testBlock {
    NSString *strBlock = @"NSStackBlock";
    [self startWithBlock:^{
        NSLog(@"%@",strBlock);
    }];
}

执行testBlock方法,控制台输出

<__NSStackBlock__: 0x7fff563aa988>

此处可能会有疑问:既然当block作为函数参数时为_NSConcreteStackBlock类型,超出其作用域时,block会被释放掉,那会不会出现函数先退出了,block还是没有执行完毕的?

经过我测试,我发现,其实在函数中,在block执行完毕前,函数是不会退出的。因为函数中按顺序执行的,函数中block后的代码会等待block执行完毕,所以在block块代码未执行完毕时,该函数不会退出,从而没有超过block的作用域,block不会被释放。看下面的例子就可以明白了。

- (void)startWithBlock:(void(^)())block {
    block();
    NSLog(@"%@",block);
}
- (void)testBlock {
    NSString *strBlock = @"NSStackBlock";
    [self startWithBlock:^{
        NSLog(@"%@",strBlock);
    }];
}

控制台输出

NSStackBlock
<__NSStackBlock__: 0x7fff54ba4a20>

从打印结果可以看出,当我们执行startWithBlock函数时,先是执行了block内的代码,再是执行函数中block后的代码,所以可以保证block执行完毕。

可能,还有人会问,如果把block()放在子线程中执行呢,这样就不是按顺序执行了,在block块代码执行之前,函数就退出了,这样是不是block就不能执行完毕呢?

其实,把block放子线程中,无非是通过GCD和performSelectorInBackground方法,系统会自动GCD的block进copy操作,而performSelectorInBackground需要传一个selector,又相当于走进了函数里,还是按顺序执行了,函数还是会等待block执行完毕。

针对不同block类型的copy、retain、release操作

注:尽量不要对block使用retain操作。因为从上可以看出,retain操作对)_NSConcreteStackBlock并没有效果,这样会误以为retain生效了,在后续调用block的时候,其实block早就被释放了,从而导致crash

block循环引用问题

可以使用__weak__unsafe_unretained__block修饰词修饰被block持有的对象来打破循环,还有就是在block执行完毕的时候,将block置nil的方法。具体细节这里就不讲了,有兴趣的童鞋可以看看我简书上写了另一篇文章

上一篇下一篇

猜你喜欢

热点阅读