block(一):block的本质

1. 简单概述

block 是 C 语言的扩充功能,我们可以认为它是 带有自动变量的匿名函数
,同时也是一个 对象

block 是一个匿名的 inline 代码集合,有如下特点:

  • 参数列表,就像一个函数(看起来是个函数,执行起来像是一个函数);
  • 是一个对象;
  • 有声明的返回类型。

2. block怎么写

最简单的写法。

int (^DefaultBlock1)(int) = ^int (int a) {
return a + 1;
};
DefaultBlock1(1);
复制代码

升级版。

// 利用 typedef 声明block
typedef return_type (^BlockTypeName)(var_type);
// 作属性
@property (nonatomic, copy ,nullable) BlockTypeName blockName;
// 作方法参数
- (void)requestForSomething:(Model)model handle:(BlockTypeName)handle;
复制代码

3. block的实现

在LLVM的文件中,我找到了一份文档, Block_private.h
,这里可以查看到block的实现情况

  • 注:实际上真实的代码结构和使用 clang 指令转换过来的代码,是有可能不一样的。

    struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, …);
    struct Block_descriptor descriptor;
    /

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

里面的 invoke 就是指向具体实现的函数指针,当 block 被调用的时候,程序最终会跳转到这个函数指针指向的代码区。

Block_descriptor
里面最重要的就是 copy
函数和 dispose
函数,从命名上可以推断出,copy 函数是用来 捕获变量并持有引用
,而 dispose 函数是用来 释放捕获的变量
。函数捕获的变量会存储在结构体 Block_layout
的后面,在 invoke 函数执行前全部读出。

不过光看文档并不直观。我们使用 clang -rewrite-objc
将一份 block 代码进行编译转换,将得到一份C++代码。刨除其他无用的代码:

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* 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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
}
return 0;
}
复制代码

先看最直接的 __block_impl
代码,

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
复制代码

这是一个结构体,里面的元素分别是

  • isa:
    指向所属类的指针,也就是 block 的类型
  • flags
    标志变量,在实现 block 的内部操作时会用到
  • Reserved
    保留变量
  • FuncPtr
    block 执行时调用的函数指针

接着, __main_block_impl_0
因为包含了 __block_impl ,我们可以将它打开,直接看成

__main_block_impl_0{
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0 *Desc;
}
复制代码

通过观察它,我们可以将 block 理解为,一个对象,内部包含一个函数。

4. block的类型

我们常见的block是有三种:

  • __NSGlobalBlock
  • __NSStackBlock
  • __NSMallocBlock

比如说

void (^block)(void) = ^{
NSLog(@"biboyang");
};
block();
复制代码

或者

static int age = 10;
void(^block)(void) = ^{
NSLog(@"Hello, World! %d",age);
};
block();
复制代码

像是这种,没有对外捕获变量的,就是 GlobaBlock 。

而我们在写一个捕获变量的。

int b = 10;
void(^block2)(void) = ^{
NSLog(@"Hello, World! %d",b);
};
block2();
复制代码

这种 block,在 MRC 中,是 StackBlock 。在 ARC 中,因为编译器做了优化,自动进行了 copy ,这种就是 MallocBlock 了。

做这种优化的原因很好理解:

如果 StackBlock 访问了一个自动变量,因为自己是存在栈上的,所以变量也就会被保存在栈上。但是因为栈上的数据是由系统自动进行管理的,随时都有可能被回收,非常容易造成野指针的问题。

那该如何解决呢?复制到堆上就好了!

ARC 机制也确实这么做的。它会自动将栈上的 block 复制到堆上,所以,ARC 下的 block 的属性关键词其实使用 strong 和 copy 都不会有问题,不过为了习惯,还是使用 copy 为好。

Blcok 的类

副本源的配置存储域

复制效果

__NSStackBlock

__NSGlobalBlock

程序的数据区域

无用

__NSMallocBlock

引用计数增加

系统默认调用 copy 方法把 block 复制的四种情况

  1. 手动调用 copy
  2. block 是函数的返回值
  3. block 被强引用,block 被赋值给 __strong 或者 id 类型
  4. 调用系统 API 入参中含有 usingBlcok 的 Cocoa 方法或者 GCD 的相关 API

ARC 环境下,一旦 block 赋值就会触发 copy,block 就会 copy 到堆上,block也就会变成 __NSMallocBlock 。当然,如果刻意的去写(没有实际用处),ARC 环境下也是存在 __NSStackBlock 的,这种情况下,block 就在栈上。

从报错看内存

如果我们把 block 设置为 nil ,然后去调用,会发生什么?

void (^block)(void) = nil;
block();
复制代码

当我们运行的时候,它会崩溃,报错信息为 Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)

我们可以发现,当把 block 置为 nil 的时候,第四行的函数指针,被置为 NULL ,注意,这里是 NULL 而不是 nil 。

我们给一个对象发送 nil 消息是没有问题的,但是给如果是 NULL 就会发生崩溃。

  • nil:指向oc中对象的空指针
  • Nil:指向oc中类的空指针
  • NULL:指向其他类型的空指针,如一个c类型的内存指针
  • NSNull:在集合对象中,表示空值的对象
  • 若obj为 nil:[obj message] 将返回NO,而不是NSException
  • 若obj为 NSNull:[obj message] 将抛出异常NSException

它直接访问到了函数指针,因为前三位分别是 void、int、int,大小分别是 8、4、4,加一块就为 16 ,所以在 64 位中,就表示出 0x10 地址的崩溃。 如果是在 32 位的系统中,void 的大小是 4,崩溃的地址应该就是 0x0c。

收录地址

稀土掘金
我还没有学会写个人说明!
上一篇

《流畅的Python》第十章学习笔记

你也可能喜欢

评论已经被关闭。

插入图片