Block学习

2020-02-25  本文已影响0人  Tony17

前言

Block 是 C 语言的扩充功能,可以用一句话来表示Blocks的扩充功能:带有局部变量(自动变量)的匿名函数。
Block 是在开发过程中使用非常频繁。它可以在需要使用的时候直接调用,非常好的保持了上下文的关系。但是我对于它的具体实现一直不是很清楚。
简单调用代码:

#define block ^{NSLog(@"===== block1 age: %d", age);}

#import <Foundation/Foundation.h>
static int weight = 20;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block1)(void)= ^{
            NSLog(@"===== block1");
        };
        int age = 10;
        void(^block2)(void)= block;
        void(^block3)(void)= ^{
            NSLog(@"===== block2, weight: %d", weight);
        };
        NSLog(@"\n直接创建: \t%@\n直接创建+copy: \t%@\n创建并无调用变量: \t%@\n创建并调用局部变量: \t%@\n创建并调用全局变量: \t%@",[block class], [[block copy] class], [block1 class], [block2 class], [block3 class]);
    }
    return 0;
}

输出内容:

直接创建:   __NSStackBlock__
直接创建+copy:  __NSMallocBlock__
创建并无调用变量:   __NSGlobalBlock__
创建并调用局部变量:  __NSMallocBlock__
创建并调用全局变量:  __NSGlobalBlock__

字段说明

匿名函数

Block 表达式

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

所以Block 的常见写法有如下几种:

^int (int count) {return count + 1;}    // 有返回值类型,有参数列表
^(int count) {return count + 1;}        // 没有有返回值类型,有参数列表
^int {return 1;}          // 有返回值类型,没有参数列表
^ {NSLog(@"Block");}    // 没有有返回值类型,没有参数列表

Block 类型

类型 名称 存放位置 环境
NSGlobalBlock 全局Block data区 - 全局访问 没有访问auto变量
NSStackBlock 栈Block 栈区 - 自动释放 访问了auto变量
NSMallocBlock 堆Block 堆区 - 手动释放 栈Block 调用了 copy

一般来说,在 ARC 模式下,把 Block 赋值给变量的时候,系统会自动调用 copy 操作把 Block 复制到堆上,所以基本上不会访问到栈Block的。这里把 Block 放在堆上的主要原因就是 栈区的变量在超出生命周期之后自动释放,很容易导致 Block 还没有调用就已经释放了。

自动变量

在 Block 中,表达式会截获所有使用的自动变量的值(保存该自动变量的瞬时值)。这个值的捕获时机是在 Block 定义的时候,所以在 Block 定义后再修改变量的值是不会对 Block 内部使用的值造成影响的。

但是 Block 自动捕获的变量是不可以修改的。如果需要修改这个变量并且保持 Block 内外值一致。就需要在外部定义变量的时候使用 __block 来修饰。这里需要注意一下,操作可变对象是可以的。给可变对象重新赋值是不可以的。

NSMutableArray *array = [NSMutableArray new];
void (^blk)(void) = ^ {
    [array addObject:[NSObject new]];
};
blk();
NSLog(@"%lu", (unsigned long)array.count);

这段代码是没有问题的,因为虽然是在修改变量 array 的内部值,但是 array 本身并没有变。

NSMutableArray *array = [NSMutableArray new];
void (^blk)(void) = ^ {
    array = [NSMutableArray new];
};
blk();
NSLog(@"%lu", (unsigned long)array.count);

这段代码是会报错,xcode 的错误信息是 Variable is not assignable (missing __block type specifier)。因为这里是给变量 array 重新赋值了。

另外,在使用 Block 的时候要注意 C 语言的数组,由于自动捕获机制没有针对 C 语言数据的实现。所以如果有这方面的需求的话,需要使用指针来解决这个问题。

Block 实质

通过查看源码可以知道,Block 的大致结构如下:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};
struct __Block_byref_obj_1 {
  void *__isa;
__Block_byref_obj_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *obj;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_obj_1 *obj; // by ref
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_obj_1 *_obj, __Block_byref_age_0 *_age, int flags=0) : obj(_obj->__forwarding), age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__block

在开发过程中,如果想让 block 内外的变量值保持一致,就需要使用 __block 来修饰定义的变量。

__block 修饰的变量最大的特点是在生成的结构体中存在 __forwarding 变量,这个变量始终指向变量的实际地址。例如 Block 从栈copy到堆上的时候,栈区定义的变量值也会复制到堆上,所以对应的__block 结构体中的 __forwarding 指向堆中的地址。这样每次访问这个变量的时候,都会访问 __forwarding 指针指向的内存地址的值,从而保持值一致。

__weak

block使用过程中,最常见的一个问题就是循环引用,循环引用的定义和引发的后果这里就不多做阐述。而避免循环引用的方式也很简单,就是在外部使用 __weak 修饰符申明一下,block 内部引用的是这个 __weak 修饰的变量。但是这样也会有一个问题,就是如果在 block内部调用的时候,这个对象有可能已经释放了。这样会导致原本的逻辑遭到破坏,这时候有人提出来,完美的解决办法就是 block 外部使用 __weak 修饰一下,block 内部 使用 __strong 重新赋值一下,这样就可以在使用的时候保证这个值的存在了。

最后

欢迎斧正~

上一篇 下一篇

猜你喜欢

热点阅读