我所理解的 Block

做者:bool周 原文连接:我所理解的 Blockhtml

关于 block 的文章,网上已经有不少了。我这里只是将这个知识点再梳理一下,从 block 使用到底层原理。毕竟年纪大了,容易忘事。编程

抛砖引玉

围绕 block 所产生的问题,太多太多。这里我将这些问题罗列出来,若是你对某些问题感到懵逼,能够在下文中找到答案。找不到,私信我。数组

  • 为何要用 block?毕竟它的语法难记,还容易产生内存泄漏。
  • block 的各类书写格式,你是否了解?
  • 按内存区这一维度划分,block 能够分为哪几种类型,如何定义的?
  • block 是 Objective-C 对象吗?
  • block 内部实现原理是怎样的?
  • 怎样写会形成循环引用,又是如何避免循环引用?
  • 若是以上问题你都了解,能够不用往下看了。

为何使用 Block

block 的惟一好处就是:使代码变得更简洁bash

咱们能够向一个方法以参数的形式传递一个 block,做为方法的 callback 函数。相似于向方法传递一个函数指针。这样就没必要再声明一个新的方法,并调用,在必定程度上简化了代码。下面有一个例子:app

使用 notification 时,常规方式是注册一个 selector 并实现对应的方法,像这样:框架

- (void)viewDidLoad {
   [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(keyboardWillShow:)
        name:UIKeyboardWillShowNotification object:nil];
}
 
- (void)keyboardWillShow:(NSNotification *)notification {
    // Notification-handling code goes here.
}
复制代码

若是使用 block,能够写成这样:async

- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification
         object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
             // Notification-handling code goes here. 
    }];
}
复制代码

另一个简化代码的特性就是,block 能够捕获外部变量。这样就没必要再以参数的形式传递,简化的方法的定义和调用。ide

Block 长什么样

在最初接触 block 时,我常常写不对,它的语法太另类。fucking block syntax 提供了各类 block 的写法,我这里就直接照搬过来了。函数

  • 做为局部变量
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
复制代码
  • 做为属性(property)
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);
复制代码
  • 定义方法时,做为方法参数
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
复制代码
  • 调用方法时,做为参数传递
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];
复制代码
  • 做为类型别名 (typedef),增长代码可读性
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};
复制代码

Block 内部原理是怎样的

在编译时,编译器会将 block 语法转化成 C 的源代码,再将这部分 C 的源代码编译为编译器处理的代码。咱们可使用 clange (LLVM 编译器) 来完成 "将 block 语法转化为 C++ 源代码 (本质仍是 C)" 这一阶段。具体命令以下:ui

clang -rewrite-objc 源代码文件名
复制代码
1.一个简单 Block 的结构

下面咱们转化一段 OC 代码来分析 block。

使用 clang -rewrite-objc main.m 转化以下代码:

int main(int argc, char * argv[]) {
    void (^myBlock) (void) = ^{printf("test block");};
    myBlock();
    
    return 0;
}
复制代码

转化接入后是下面这个样子(主要代码)。由于语法和命名的关系,代码看着很乱,可是逻辑很清晰。为了方便理解,我加了部分注释。

// block 结构体。能够理解为 'block' 这种类型的基本结构
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

// 整个 block 的结构,命名有点歧义,理解便可
struct __main_block_impl_0 {
  struct __block_impl impl;	 // __block_impl 类型的成员变量
  struct __main_block_desc_0* Desc; // Desc 指针
  
  // 构造函数主要是为两个成员变量赋值
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// block 的代码块,实际执行部分
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	printf("test block");
}

// 版本升级所需的区域和 block 大小。不懂也不要紧
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

// main 方法
int main(int argc, char * argv[]) {

	 // 定义 block
    void (*myBlock) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
    // 执行 block
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}
复制代码

上述代码中,定义了三个结构体:block 基本结构 __block_impl、Desc 指针 __main_block_desc_0、整个 block 的结构 __main_block_impl_0。其中 __main_block_impl_0 包含两个成员变量,分别为 __block_impl 结构体实例和 __main_block_desc_0 指针。

上述还定义了两个方法:block 实际执行方法 __main_block_func_0main() 方法。

__main_block_func_0 方法为输出对应的字符串("test block")。

main 方法主要分为两步:

定义 block。将 block 实际执行方法,也就是 __main_block_func_0 的函数指针和 __main_block_desc_0_DATA 的地址传入 __main_block_desc_0 的构造方法,构形成一个完整的 block。根据定义能够看出 __main_block_desc_0 初始化时全部的大小为 __main_block_impl_0 结构体大小。

执行 block。实际能够简化为 *myBlock->impl.FuncPtr,就是调用对应的方法。

了解了这个基本结构,后面的都是在这基础上追加部分代码,很容易理解。

2.Block 结构与 isa 指针

在上述代码中,咱们能够看出 block 结构体,也就是 __block_impl 中有一个 isa 指针。咱们先来看看这个 isa 指针。

“id" 这一变量类型用于存储 OC 对象。在 runtime.h 中,它的定义以下:

typedef struct objc_objct {
	Class isa;
} *id;
复制代码

Class 类型属于一个结构体指针类型,定义为:

typedef struct objc_class *Class
复制代码

objc_class 结构体定义以下:

struct objc_class {
	Class isa;
}
复制代码

综上可知,OC 中每一个类的结构体就是基于 objc_class 结构体。

在上面能够看到这样一段代码:

impl.isa = &_NSConcreteStackBlock;
复制代码

isa 被赋值为 _NSConcreteStackBlock 类型的指针。那么 _NSConcreteStackBlock 又是什么?经过 debug 界面咱们能够看到以下状况 :

block 一供有三种类型,分别为 __NSGlobalBlock____NSStackBlock____NSMallocBlock__,这三种类型后面会详细解释。这里转化的代码和 debug 界面显示的类型不同,可是基本类型以信仰,都是 Class 类型,没必要纠结。

能够看出 _NSConcreteStackBlock 实际是 Class 类型。那么,block 本质就是 Objective-c 对象

3.捕获自动变量

咱们将源代码改成以下状况:

int main(int argc, char * argv[]) {
	 int val = 10;
    void (^myBlock) (void) = ^{printf("value is %i", val);};
    myBlock();
    
    return 0;
}
复制代码

使用 clang 进行转化。咱们只看转化后的关键部分。即整个 block 结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; 
	printf("value is %i", val);
}
复制代码

能够看到局部变量 val 被自动追加到了 __main_block_impl_0 结构体中,并在构造函数中添加了参数。经过构造函数初始化 block 时,会将外部变量捕获进来。这里捕获的是引用,因此在 block 内部改变局部变量的值以后,并不会传出去

4.关于 __block

正常状况下,block 捕获的变量是不能够修改的。可是有两种方式可让其修改:

  • 使用静态变量、静态全局变量、全局变量。由于前两个生成在静态数据区,最后一个生成在堆区。它们都不会随着 block 栈的消失而被释放。出了 block 做用域依然有效。可是平时使用这种变量诸多不变。
  • 使用 __block 关键字修饰。它相似于 staticautoregister 这些关键字,主要来指定变量存储在哪一个区域。

为何使用 __block 关键字修饰以后就能够修改。咱们使用 clang 转化以下一段代码:

int main(int argc, char * argv[]) {
	 __block int val = 10;
    void (^myBlock) (void) = ^{val = 20;};
    myBlock();
    
    return 0;
}
复制代码

转换后以下,能够看出加了一句 __block 多了不少代码,依然是代码很乱,可是逻辑很清晰,咱们只看主要部分 :

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  	__Block_byref_val_0 *val = __cself->val; // bound by ref
	(val->__forwarding->val) = 20;
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
	_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
	_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    
    void (*myBlock) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}
复制代码

咱们能够看出局部变量转化为一个结构体:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};
复制代码

__main_block_impl_0 中追加了一个 __Block_byref_val_0 结构体指针,后续的初始化和修改 val 的值也是经过指针来操做。因此修改后的值就能够传出去了。

5.block 的存储类型

前面有提到过,block 按照存储类型划分,能够分为三种:

  • _NSConcreteGlobalBlock
  • _NSConcreteStackBlock
  • _NSConcreteMallocBlock

他们在内存中的存储结构以下图所示,对号入座:

咱们分别来解释一下。

**_NSConcreteGlobalBlock,也叫全局 block。**有两种生成方式: 一种是在全局的地方生成,不存在捕获局部变量的状况。例如:

void(^globalBlock)(void) = ^{printf("this is global block");};

int main(int argc, char * argv[]) {
    globalBlock();
    return 0;
}
复制代码

另外一种是,block 中不截获局部变量。例如:

typedef int (^TestBlock) (int);
int main(int argc, char * argv[]) {
    TestBlock block = ^(int num) {printf("num is %d",num);}
    return 0;
}
复制代码

**_NSConcreteStackBlock,也叫栈 block。**除了上述的初始化方式,经过其余方式初始化为的 block 都是栈 block。

_NSConcreteMallocBlock,也叫堆 block。 堆 block 不是由代码初始化来的,而是由栈 block 调用 copy 方法时从栈内存拷贝到堆内存而得来的。

至于何时会发生 copy 操做,能够总结为一下几点 (ARC 环境):

  • Cocoa 框架的方法且方法名中含有 usingBlock。
  • GCD 中的方法。
  • block 赋值给强引用对象时。
  • 做为返回值时。
  • 显示调用 copy 方法。

下面是一些例子:

typedef BOOL (^TestBlock)(NSString *);
typedef void (^paramBlock)(NSString *);

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    int val = 10;
    
    // block1 is global block
    void (^block1)(NSString *) = ^(NSString *name) {
        NSLog(@"this is global block");
    };
    
    // block2 is malloc block
    void (^block2)(NSString *) = ^(NSString *name) {
        int value = 10 * val;
        NSLog(@"this is malloc block");
    };
    
    // block3 is stack block
    __weak void (^block3)(NSString *) = ^(NSString *name) {
        int value = 10 * val;
        NSLog(@"this is stack block");
    };
    
    // block4 is malloc block
    TestBlock block4 = [self testWithBlock:^(NSString *name) {
        NSLog(@"noting");
    }];
    
    // block5 is global block
    TestBlock block5 = [self getGlobalBlock];

}

- (TestBlock)testWithBlock:(paramBlock)block {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"capture block is %@",block); // malloc block
    });
    
    int val = 10;
    return ^BOOL(NSString *name) {
    		int value = val * 10;
        NSLog(@"noting");
        return YES;
    };
}

- (TestBlock)getGlobalBlock {
    return ^BOOL(NSString *name) {
        NSLog(@"nothing");
        return YES;
    };
}
复制代码
6. __block 变量结构中的 __forwarding

在前面的代码中,咱们发现 __block 代码中有一个 __forwarding,以下面的代码:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};
复制代码

长话短说。当一个栈 block 捕获了一个在栈上生成的 __block 变量,那么随着 block 从栈上 copy 到堆上,这个 __block 变量也从栈上 copy 到堆上。由于有一个 __forwarding 指针,使得不管从从栈上仍是堆上,访问的都是一个变量。若是没有明白看下面的图和代码。

__block int val = 10;

void (^block)(int) = [^(int count) { val++;} copy];

val++;
block();

NSLog(@"val is %d",val); // val is 12;
复制代码

不管是操做栈上的 val 变量仍是堆上的 val 变量,最终修改的是同一个值。

7.block 与循环引用

发生循环引用说明出现了互相持有的现象,例以下面这样:

上图中 self 持有 blk 属性,blk 持有 block,block 持有 self,这就造成了一个环。现现在的 Xcode 已经很智能,这种简单的循环引用,会出现警告。

为避免循环引用,可使用 __weak 关键字。例以下面这样:

__weak typeof(self) weakSelf = self;

self.blk = ^BOOL(NSString *name) {
   [weakSelf log];
   return YES;
};
复制代码

为了不在 block 内使用 self 期间,self 被释放。能够在 block 内部对 self 进行强引用。由于这个强引用生成在 block 栈内,会随着 block 的做用域消失而消失。不会产生循环引用。

__weak typeof(self) weakSelf = self;

self.blk = ^BOOL(NSString *name) {
	__strong typeof(self) self = weakSelf;
   [self log];
   return YES;
};
复制代码

如何使用 Block

前面讲了不少原理,过程当中也讲了不少使用。这里只总结几点,使用 block 必定要注意:

  • block 的命名方式,牢记。
  • 对于要再 block 内修改的变量,加 __block 修饰符。对于 OC 中的一些对象,例如 NSMutableArray,若是只修改数组内的元素,不须要加 __block;若是要修改数组的指针,须要加 __block
  • 使用自定义 block 时,注意循环引用的问题。尤为是各类间接关系产生的循环引用。
  • 对于捕获到 block 中的弱引用,若是怕使用期间被释放,须要再 block 内部再次强引用一下。

综上,block 总结完毕,祝好运。

参考文献

1.A Short Practical Guide to Blocks

2.How Do I Declare A Block in Objective-C?

3.Objective-C高级编程

相关文章
相关标签/搜索