0%

iOS底层探索 - Block

block是我们开发经常遇到的一个结构,本篇我们就来探索一下它的结构。

block的分类

block的分类相信大家已经很清楚了,分为全局block、堆block和栈block。我们来个例子看看它们的区别

  • 全局block

    1
    2
    3
    4
    5
    void (^block)(void) = ^{
    };
    NSLog(@"%@",block);
    ///打印结果
    <__NSGlobalBlock__: 0x1072d4100>

    全局block是指不捕获任何外部变量的block,只会使用静态变量和全局变量,存储于内存的全局区

  • 堆block

    1
    2
    3
    4
    5
    6
    7
    int a = 10;
    void (^block)(void) = ^{
    NSLog(@"Cooci - %d",a);
    };
    NSLog(@"%@",block);
    ///打印结果
    <__NSMallocBlock__: 0x60000130c4b0>

    堆block会捕获外部变量,存储于内存的堆区

  • 栈block

    1
    2
    3
    4
    5
    6
    7
    int a = 10;
    void (^__weak block)(void) = ^{
    NSLog(@"Cooci - %d",a);
    };
    NSLog(@"%@",block);
    ///打印结果
    <__NSStackBlock__: 0x7ffee06ac4d8>

    栈block也会捕获外部变量,和堆block的区别是需要加__weak修饰,它存储于内存的栈区

block的循环引用

block引起循环引用的原因

A、B相互持有,所以导致A无法调用dealloc方法给B发送release信号,而B也无法接收到release信号。所以A、B此时都无法释放。如图所示

循环引用

解决循环引用

我们看一段循环引用的代码

1
2
3
4
5
NSString *name = @"JS";
self.block = ^(void){
NSLog(@"%@",self.name);
};
self.block();

这段代码出现了循环引用,因为在block内部使用了外部变量name,导致block持有了self,而self原本是持有block的,所以导致了self和block的相互持有

解决方法:

  • __weak和__strong组合使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    typedef void(^JSBlock)(void);

    @property(nonatomic, copy) JSBlock jslBlock;

    __weak typeof(self) weakSelf = self;
    self.jslBlock = ^(void){
    __strong typeof(weakSelf) strongSelf = weakSelf;
    NSLog(@"%@",weakSelf.name);
    }
    self.jslBlock();

    这是我们最容易想到的方式,使用__weak打破强引用,__strong的作用的方式self提前释放,而block执行的时候因为self已经释放而拿不到值。

  • __block定义一个临时变量指向self。

    1
    2
    3
    4
    5
    6
    7
    8
    __block ViewController *vc = self;
    self.jslBlock = ^(void){
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"%@",vc.name);
    vc = nil;//需手动释放
    });
    };
    self.jslBlock();

    这种方式是在方法外部定义一个指向self的变量,block内部捕获临时变量,使用结束后将临时变量置为nil,加__block的原因是需要在block内存对其进行置空操作。

  • block加一个参数,使用参数

    1
    2
    3
    4
    5
    6
    7
    8
    typedef void(^JSBlock)(ViewController *);
    @property(nonatomic, copy) JSBlock jslBlock;
    self.jslBlock = ^(ViewController *vc){
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"%@",vc.name);
    });
    };
    self.jslBlock(self);

Block的底层分析

我们主要通过clang和断点调试的方式分析。

xcrun编译分析

我们首先自定义一个block.c文件

1
2
3
4
5
6
7
8
9
#include "stdio.h"
int main(){
int a = 18;
void(^block)(void) = ^{
printf("js - %d",a);
};
block();
return 0;
}

使用xcrun命令讲block.c编译成block.cppxcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("js - %d",a);
}
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 a = 18;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}

我们把类型强转的代码去掉:

1
2
3
4
5
6
int main(){
int a = 18;
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a));
block->FuncPtr(block);
return 0;
}

简化之后我们看到block代码块是一个__main_block_impl_0__main_block_impl_0的结构是一个结构体,它的impl也是一个结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
///
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

注意:在结构体内部捕获到了外部变量a,且在结构体内部生成了一个成员变量a与其对应。

我们对代码做一下修改:

1
2
3
4
5
6
7
8
9
int main(){
__block int a = 18;
void(^block)(void) = ^{
a++;
printf("js - %d",a);
};
block();
return 0;
}

重新xcrun一下看看结果:

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
  • 可以看到,现在的a和之前的a不同的是加了__Block_byref_a_0 *修饰,这样就可以对捕获到的变量进行修改,传给block是a的地址,所以block内部可以修改。

  • impl.isa = &_NSConcreteStackBlock说明现在栈类型根据我们前面的分析这里应该是堆block,为什么不同呢。

  • fp是一个函数式保存,如果不调用不会执行。

源码分析

我们首先用汇编,查看源码在哪个库中,我们打断点

断点

然后看汇编代码,源码定位

我们添加符号断点objc_retainBlock:

1629984483869

所以我们去libobjc去搜索objc_retainBlock

1
2
3
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}

实际调用了_Block_copy,在libobjc库中并没有找到方法的实现,我们继续打符号断点:

1629984996709

_Block_copy函数的实现在libsystem库中,这个库没有开源,我们找一个替换的库libclosure的源码分析。我们在libclosure源码中搜索_Block_copy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {// 栈 - 堆 运行时拷贝到堆上
// Its a stack block. Make a copy.
size_t size = Block_size(aBlock);
struct Block_layout *result = (struct Block_layout *)malloc(size);
if (!result) return NULL;
memmove(result, aBlock, size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;

#if __has_feature(ptrauth_signed_block_descriptors)
if (aBlock->flags & BLOCK_SMALL_DESCRIPTOR) {
uintptr_t oldDesc = ptrauth_blend_discriminator(
&aBlock->descriptor,
_Block_descriptor_ptrauth_discriminator);
uintptr_t newDesc = ptrauth_blend_discriminator(
&result->descriptor,
_Block_descriptor_ptrauth_discriminator);

result->descriptor =
ptrauth_auth_and_resign(aBlock->descriptor,
ptrauth_key_asda, oldDesc,
ptrauth_key_asda, newDesc);
}
#endif
#endif
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}

其中Block_layout结构体的结构是:

1
2
3
4
5
6
7
8
struct Block_layout {
void * __ptrauth_objc_isa_pointer isa;//isa 标识是栈、堆block类型
volatile int32_t flags; // contains ref count 引用计数
int32_t reserved;//流程数据
BlockInvokeFunction invoke;//调用函数
struct Block_descriptor_1 *descriptor;//相关描述
// imported variables
};

对结构有个简单了解之后,我们打符号断点,看运行中block的结构。

  • objc_retainBlock

    1629986289286

    发现此时的block类型还是StackBlock

  • _Block_copy最后打一个断点:

    1629986567227

    此时block的类型就是__NSMallocBlock__类型了。

blockLayout结构
1
2
3
4
5
6
7
8
struct Block_layout {
void * __ptrauth_objc_isa_pointer isa;//isa 标识是栈、堆block类型
volatile int32_t flags; // contains ref count 引用计数
int32_t reserved;//流程数据
BlockInvokeFunction invoke;//调用函数
struct Block_descriptor_1 *descriptor;//相关描述
// imported variables
};

我们看Block_descriptor_1的结构,发现并没有上面调试打印的signature信息。

我们看源码发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;//拷贝函数指针
BlockDisposeFunction dispose;
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;//签名
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT 布局
}

Block_descriptor_2Block_descriptor_3是可选的,它们是通过Block_descriptor_1内存平移得到的。

捕获变量的copy
_Block_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
// 栈Block -> 堆Block
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;

if (!arg) return NULL;

// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;//强转为Block_layout类型对象,防止对外界造成影响
if (aBlock->flags & BLOCK_NEEDS_FREE) {//是否需要释放
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {//如果是全局block,直接返回
return aBlock;
}
else {//为栈block 或者 堆block,由于堆区需要申请内存,所以只可能是栈区
// Its a stack block. Make a copy. 它是一个堆栈块block,拷贝。
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);//申请空间并接收
if (!result) return NULL;
//通过memmove内存拷贝,将 aBlock 拷贝至result
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;//可以直接调起invoke
#endif
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed 告知可释放
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;//设置block对象类型为堆区block
return result;
}
}

_Block_copy主要是将block从栈区拷贝到堆区

  • 如果需要释放,则直接释放
  • 如果是globalBlock不需要copy,返回
  • 剩下两种情况:堆区block和栈区block。由于堆区block需要申请内存,这里到这里只能是栈block。
    • 通过malloc申请内存空间用于接收block
    • 通过remove将block拷贝至新申请的内存中
    • 设置block对象的类型为堆区block。将isa指向__NSConcreteMallocBlock
_Block_object_assign

先看一个枚举的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Block 捕获的外界变量的种类
// Runtime support functions used by compiler when generating copy/dispose helpers
// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
// see function implementation for a more complete description of these fields and combinations
//普通对象,即没有其他的引用类型
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
//block类型作为变量
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
//经过__block修饰的变量
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
//weak 弱引用变量
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
//返回的调用对象 - 处理block_byref内部对象内存会加的一个额外标记,配合flags一起使用
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};

其中用的最多的是BLOCK_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static struct Block_byref *_Block_byref_copy(const void *arg) {
//强转为Block_byref结构体类型,保存一份
struct Block_byref *src = (struct Block_byref *)arg;

if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack 申请内存
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
//block内部持有的Block_byref 和 外界的Block_byref 所持有的对象是同一个,这也是为什么__block修饰的变量具有修改能力
//copy 和 scr 的地址指针达到了完美的同一份拷贝,目前只有持有能力
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
//如果有copy能力
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
//Block_byref_2是结构体,__block修饰的可能是对象,对象通过byref_keep保存,在合适的时机进行调用
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;

if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
//等价于 __Block_byref_id_object_copy
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}

_Block_object_assign是在底层编译代码中,外部变量拷贝时调用的方法就是它。

  • 如果是普通对象,交给系统arc处理,拷贝对象指针,引用技术+1,外界变量不能释放。
  • 如果是block类型的变量,通过_Block_copy操作,将block从栈区拷贝到堆区。
  • 如果是__block修饰的变量,调用_Block_byref_copy函数,进行内存拷贝以及常规处理。
_Block_byref_copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static struct Block_byref *_Block_byref_copy(const void *arg) {
//强转为Block_byref结构体类型,保存一份
struct Block_byref *src = (struct Block_byref *)arg;

if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack 申请内存
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
//block内部持有的Block_byref 和 外界的Block_byref 所持有的对象是同一个,这也是为什么__block修饰的变量具有修改能力
//copy 和 scr 的地址指针达到了完美的同一份拷贝,目前只有持有能力
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
//如果有copy能力
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
//Block_byref_2是结构体,__block修饰的可能是对象,对象通过byref_keep保存,在合适的时机进行调用
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;

if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
//等价于 __Block_byref_id_object_copy
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
  • 将传入的对象,强转为Block_byref结构体类型对象。保存
  • 如果没有将变量拷贝到堆上,就申请内存进行拷贝
  • 如果已经拷贝,则进行处理并返回
  • 其中copy和src的forwarding指针都是指向同一片内存。这就是为什么__block修饰的对象具有修改的能力。

三层copy小结

  • 第一层:通过_Block_copy实现对象的自身拷贝,从栈区拷贝至堆区
  • 第二层:通过_Block_byref_copy方法,将对象拷贝为Block_byref结构体类型
  • 第三次:调用_Block_object_assign方法,对__block修饰的当前变量的拷贝