Objective-C Block初探

2020-03-18  本文已影响0人  收纳箱

1. 概述

一个闭包包含:

在Objective-C中,Block就是闭包。

Block有以下特点:

2. Block的定义

很多人觉得Block的定义很怪异,很难记住。但其实和C语言的函数指针的定义对比一下,你很容易就可以记住。

// Block
returnType (^blockName)(parameterTypes)

// 函数指针
returnType (*c_func)(parameterTypes)

例如输入和返回参数都是字符串:

(char *) (*c_func)(const char *);
(NSString *) (^block)(NSString *);

3. Block与外部变量

3.1 捕获自动变量(局部变量)

typedef void (^MyBlock)(void);
默认情况

对于Block外部的变量,Block默认是将其复制到数据结构中来实现访问的:

默认情况下Block只访问局部变量的值,而不需要修改它。

int age = 10;
MyBlock block = ^{
    NSLog(@"age = %d", age);
};
age = 18;
block();

//输出
10
__block修饰

如果需要修改外部变量的值,则需要对变量使用__block进行修饰。此时,Block会复制变量的引用地址来实现访问,所以可以修改外部的变量值。

__block int age = 10;
MyBlock block = ^{
    NSLog(@"age = %d", age);
};
age = 18;
block();

//输出
18

一句话就是:没有__block修饰是值引用,有__block修饰是指针引用

3.2 __block修饰原理

为什么使__block修饰就可以修改变量值呢?我们创建一个main.m文件。

#import <Foundation/Foundation.h>

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[])
{
  @autoreleasepool
  {
     __block int age = 10;
     MyBlock block = ^{
         NSLog(@"age = %d", age);
     };
     age = 18;
     block();
  }
  return 0;
}

然后在终端中输入:

clang -rewrite-objc main.m

会生成一个main.cpp文件。

__block int age = 10;
...
age = 18;

//转换为
__Block_byref_age_0 age = {
   0,
   &age,
   0,
   sizeof(__Block_byref_age_0),
   10
};
...
age.__forwarding->age = 18;

我们发现__block修饰之后,局部变量age变成了__Block_byref_age_0结构体类型变量实例。

现在我们要访问age变量,则需要通过__forwarding成员变量来间接访问。

4. Block的copy操作

4.1 Block的存储域

Block是存储在栈上还是堆上呢?

其实,Block有三种不同的类型:

Block存储区域

看到一个Block,我们如何确认Block的存储区域呢?

Block不访问外界变量(包括栈和堆中的变量)

MRC和ARC中都一样,Block在代码段中,即全区块。

Block访问外界变量

MRC:默认存储在栈区

ARC:默认存储在栈区,然后ARC情况下,按需自动拷贝到了堆区,自动释放。

4.2 Block的copy操作

那么问题来了:为什么ARC下,访问外界变量的Block会自动拷贝到堆区呢?

栈块存在于栈内存中,超出作用域后会被销毁。使用__block的变量也会被回收。为了解决这个问题,需要把栈块复制到堆上,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断,是否需要将Block从栈复制到堆。如果需要,自动生成复制代码。Block的复制操作执行的是copy实例方法。Block调用了copy方法就会从栈块变成堆块。

例如下面这个返回类型为blk_tBlock的函数:

typedef int (^blk_t)(int);

blk_t func(int rate) {
    return ^(int count) { return rate * count; };
}

返回之前的Block是在栈上的,返回之后作用域结束,会被释放。ARC开启时,编译器就会自动完成复制。而MRC时,需要我们执行copy方法手动执行。

将Block从栈上拷贝到堆上相当消耗CPU,所以Block在栈上够用,就不要执行复制了。不然就会浪费CPU资源。

Block的复制实际是调用copy实例方法,不同类型的Block执行copy效果也稍微有些不同。

Block类 副本源的存储域 复制效果
_NSConcreteGlobalBlock 程序的数据区 什么也不做
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteMallocBlock 引用计数增加

虽然栈块也是对象,但它没有引用计数。因为栈区的内存由编译器自动分配释放,不需要引用计数。

4.3 Block的__forwarding

copy之后,Block变量也会被复制到堆上。那么访问变量是访问栈上的还是堆上的呢?之前看到的__block变量的__forwarding就是解决这个问题的。

__forwarding

通过__forwarding,无论是在Block中还是Block外访问__block变量,也不管该变量是在栈上或者堆上,都可以顺利访问同一个__block变量。

5. 防止Block循环引用

某个类将Block作为自己的变量,同时又在Block内部使用了类实例对象本身,就会出现循环引用:

self.someBlock = ^{
        [self doSomething];
}

即自己持有Block,Block又捕获了自己,造成循环引用。

  1. MRC,使用__block

    __block typeof(self) blockSelf = self;
    self.someBlock = ^{
         [blockSelf doSomething];
    }
    
  2. ARC,使用__weak

    __weak typeof(self) weakSelf = self;
    self.someBlock = ^{
         [weakSelf doSomething];
    }
    

注意:在ARC中使用__block也可能出现循环应用。

typedef void(^Block)(void);
@interface SomeObj: NSObject
@property (nonatomic, copy) Block block;
@end
  
@implementation SomeObj
- (instancetype)init
{
    self = [super init];
    if (self) {
        __block typeof(self) blockSelf = self;
        self.block = ^{
            NSLog(@"Self: %@", blockSelf);
            blockSelf = nil;
        };
    }
    return self;
}

- (void)execBlock
{
    self.block();
}
@end
  
//使用类
SomeObj *obj = [[SomeObj alloc] init];
[obj execBlock]; //如果不执行execBlock,blockSelf永远不会置空,循环引用一直存在。
上一篇下一篇

猜你喜欢

热点阅读