Block笔记(四)

通过前几章的学习我们可以知道,Block转换为Block的结构体类型自动变量__block变量转换为__block变量结构体类型的自动变量,所谓结构体类型自动变量,即上生成的该结构体的实例

Block存储域

之前我们在讲Block本质的时候知道,Block也是Objective-C对象,其isa指向了NSConcreteStackBlock类,还有两个与此类似的类分别是NSConcreteGlobalBlockNSConcreteMallocBlock

NSConcreteStackBlock类的名称中含有“栈”(stack)一词,即该类的对象设置在上,同样的NSConcreteGlobalBlock类对象与全局变量一样设置在数据区域中,NSConcreteMallocBlock类对象则设置在由malloc函数分配的内存块(堆)中。

前几篇文章中我们介绍的例子都是NSConcreteStackBlock类,且都设置在栈上,在记叙全局变量的地方使用Block语法时,生成的Block为NSConcreteGlobalBlock类,例如:

void (^blk)(void) = ^{printf("Global Block\n");};

int main(){

}

代码转换如下:

struct __blk_block_impl_0 {
  struct __block_impl impl;
  struct __blk_block_desc_0* Desc;
  __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __blk_block_func_0(struct __blk_block_impl_0 *__cself) {
printf("Global Block\n");}

static struct __blk_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blk_block_desc_0_DATA = { 0, sizeof(struct __blk_block_impl_0)};
static __blk_block_impl_0 __global_blk_block_impl_0((void *)__blk_block_func_0, &__blk_block_desc_0_DATA);
void (*blk)(void) = ((void (*)())&__global_blk_block_impl_0);

int main(){


}

该Block的类为_NSConcreteGlobalBlock类,设置在程序的数据区域中,在使用全局变量的地方不能使用自动变量,所以也就不存在对自动变量进行截获,由此Block结构体实例的内容不依赖于执行时的状态,所以整个程序中只需要一个实例。因此Block即被设置在于全局变量相同的数据区域中。

在记叙全局变量的地方有Block语法时以及Block语法表达式不使用应截获的自动变量时Block为_NSConcreteGlobalBlock类对象,除此之外Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在上。

那么NSConcreteMallocBlock类在何时使用呢?

从栈复制到堆

之前一篇我们提到过,由Block语法生成的值Block上可以存有超过变量作用域被截获自动变量,当变量作用域结束时,原来的自动变量被废弃,该Block就被废弃,将不能通过指针来访问原来的自动变量。由于__block变量也配置在上,如果其所属的变量作用域结束,该__block变量也会被废弃。

Block会提供将Block__block变量上复制到上的方法来解决这个问题。将配置在上的Block复制到上,这样即使Block语法记述的变量作用域结束上的Block还可以继续存在。

复制到上的Block将_ NSConcreteMallocBlock类对象写入Block的结构体实例成员变量isa

impl.isa = &_ NSConcreteMallocBlock;

__block变量结构体成员变量__forwarding可以实现无论__block变量配置在上还是上时都能够正确的访问__block变量

那么Blocks提供的方法是什么呢?实际上当ARC有效时大多数情形下编译器会恰当的进行判断,自动生成将Block从栈上复制到堆上的代码。我们就来举例说明。

typedef void (^block_t)(void);

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    block_t block = [self func];
    block();
}


-(block_t)func{
    __block int add=10;
    return ^{NSLog(@"add=%d\n",++add);};
};

@end

该源代码为返回配置在上的Block函数。即程序执行中从函数返回函数调用方时变量作用域结束,因此上的Block也被废弃。但是该源代码可以正常运行成功输出如下。

2018-10-26 13:43:34.257219+0800 testblock[43867:4966419] add=11

这是因为在ARC有效时编译器会如下转换代码:

block_t block = &__ViewController__func_block_impl_0(__ViewController__func_block_func_0, &__ViewController__func_block_desc_0_DATA, (__Block_byref_add_0 *)&add, 570425344)));

block = objc_retainBlock(block);

return objc_autoreleaseReturnValue(block);

objc_retainBlock函数实际上就是Block_copy函数。即:

//通过Block语法生成的Block,配置在栈上的Block结构体实例,赋值给相当于Block类型的变量block中
block = _Block_copy(block);
//_Block_copy函数将栈上的Block复制到堆上,复制后将堆上的地址作为指针赋值给变量block
return objc_autoreleaseReturnValue(block);
//将堆上的Block作为OC对象注册到autoreleasepool中,然后返回对象

将Block作为函数返回值时,编译器会自动生成复制到堆上的代码。

之前说大多数情况下编译器会适当的判断,在此之外的情况下需要手动生成代码,将Block从栈复制到堆上。此时我们使用“copy实例方法”

那么编译器不能进行判断的情况又是什么样呢。

1.向方法或函数的参数中传递Block时
2.Cocoa框架的方法且方法名字中含有usingBlock等时
3.GCD的API

我们再来举例说明:

typedef void (^block_t)(void);

@interface ViewController ()

@end



@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    id obj = [self getBlockArray];
    block_t block = (block_t)[obj objectAtIndex:0];
    block();
}

- (id)getBlockArray{
    int val = 10;
    return [[NSArray alloc] initWithObjects:^{NSLog(@"block0:%d",val);},^{NSLog(@"block1:%d",val);}, nil];
}

@end

getBlockArray方法在上生成两个Block,并传递给NSArray类的initWithObjects实例方法。但是在执行时会出现异常。

2018-10-26 14:12:16.621722+0800 testblock[44061:5009777] block0:10
objc[44061]: Attempt to use unknown class 0x600001e6b680.

异常

可以看到上的block为null,这是因为上的Block已经被废弃。可惜此时编译器不能判断是否需要复制,我们就需要手动复制,但是将Block从复制到是相当消耗CPU的,因此只有在必须复制的时候我们再进行手动复制。

- (id)getBlockArray{
    int val = 10;
    return [[NSArray alloc] initWithObjects:[^{NSLog(@"block0:%d",val);} copy],[^{NSLog(@"block1:%d",val);} copy], nil];
}

像这样我们的程序就可以正常的运行了。

__block变量存储域

上面我们把重点放在Block进行说明,那么对__block变量又是如何处理的呢?使用__block变量的Block从栈复制到堆上时,__block变量也会受到影响。

若在一个Block中使用__block变量,则当该Block从栈复制到堆时,使用的所有__block变量也必定配置在上,这些__block变量也会跟随Block一起复制到,此时Block持有__block变量

现在我们来看看之前说过的__forwarding成员变量,“不管__block变量配置在上还是在上,都能够正确地访问该变量”。通过Block的复制,__block变量也从复制到,此时可同时访问上的__block变量上的__block变量

    __block int val = 0;
    void (^blk)(void) = [^{++val;} copy];
    ++val;
    blk();
    NSLog(@"%d",val);

利用copy方法复制了__block变量的Block语法。Block和__block变量两者均是从栈复制到堆。

//该val为复制到堆上的__block变量
void (^blk)(void) = [^{++val;} copy];
//该val为复制前栈上的__block变量
++val;
//以上两者都可转化为如下形式
++(val.__forwarding->val);

但是上的__block变量在从复制到时,会将成员变量forwarding的值替换为复制目标上的__block变量的地址。通过这个功能,无论是在Block中,Block外使用__block变量上或者上的__block变量,都可以顺利的访问同一个__block变量

你可能感兴趣的:(Block笔记(四))