余额不足.

从源码分析Block的内存管理

字数统计: 1.6k阅读时长: 6 min
2018/11/01 Share

实际上我不知道怎么去起这个标题,文章大概讲述的是block的类型,block类型的转换以及block的释放,不管了先这样吧

block的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 *);
};

Block的结构体如以上代码所示,第一个成员就是咱们熟悉的isa,所以你可以把block当成是一个对象来看待。在iOS中只会出现_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock三种类型的block。

第二个成员flags在之后的分析中充当这重要角色,通常标识着block当前的状态。

第五个成员是对block的补充描述,包含 copy/dispose 函数。

第六个成员…哪来的第六个成员?结构体中明明只标明了5个啊?/ Imported variables. /表示block中可能会捕获一些变量。

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
@autoreleasepool {
static int val = 1;
void (^aBlock)(void) = ^{
val ++;
};
aBlock();
}
return 0;
}

写一个简单block,然后使用clang将源码重写成cpp文件clang -rewrite-objc maim.m ,同目录下得到一个main.cpp。

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
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; // bound by copy

(*val) ++;
}

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;
static int val = 1;
void (*aBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &val));
((void (*)(__block_impl *))((__block_impl *)aBlock)->FuncPtr)((__block_impl *)aBlock);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

转成C++代码后可以看清整个block的构造,看看构造函数

1
2
3
4
5
6
__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;
}

出现了一个在结构体从未出现过的变量val,这个val就是我们需要在ablock中捕获的变量。
isa指向了_NSConcreteStackBlock,证明他是一个_NSConcreteStackBlock,那么_NSConcreteMallocBlock和_NSConcreteGlobalBlock会在什么时候出现?

先说说_NSConcreteGlobalBlock,这是一个存在内存全局区的block,在编译时就已经完全完成了,通常是不去捕获任何变量的block,也有说法说只访问静态变量或者全局变量的block也可以是_NSConcreteGlobalBlock,小弟才疏学浅,尚未证明这点。

_NSConcreteStackBlock 是一个存在栈区的block,当然他的名字就已经说明了这一点,他可以访问外部变量,且没有强指针引用。生命周期由系统控制。

_NSConcreteMallocBlock 是一个存在堆区的block,通常由NSConcreteStackBlock调用_block_copy将内存移到堆区。一般有强指针引用或者使用copy修饰的block都是NSConcreteMallocBlock。当没有强指针引用时销毁。

block为什么要用copy修饰?

在MRC环境下,如果block使用retain修饰在block内访问外部变量会出现crash,因为MRC环境下block初始化后会存在栈区,没有改变外部变量的能力。
ARC环境下block会被自动copy到堆区,从clang的ARC文档中我们可以看到这样的描述

1
With the exception of retains done as part of initializing a __strong parameter variable or reading a __weak variable, whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy. The optimizer may remove such copies when it sees that the result is used only as an argument to a call.

在ARC环境下编译器会自动触发

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

即使编译器会自动将我们创建的block推入堆区,但苹果官方依然推荐我们使用copy来修饰block
之前访问过别人博客,有一条评论表示ARC环境下使用strong修饰依然会有crash的现象,但该条评论的作者已经忘了是什么情况下产生这种状况的了。但我们应该对代码保持一个敬畏之心,也许一个不经意间产生的bug就可能产生一个意想不到的后果。

block是怎么被推入堆区的?

上面说到ARC环境下的block会被自动推入堆区,大致上是objc_retainBlock()调用了_Block_copy(),那具体是怎么实现的呢?
emmm…先看看官方开源的runtime.c源码。

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
// 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;

// 判断传入的block是否为空
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
// 如果这个block是一个要被释放的block,当然这个block肯定是堆block了,既然你都有对象了,咱们也就没有继续谈下去的必要了,return
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
// 如果是个全局block,return
return aBlock;
}
else {
// Its a stack block. Make a copy.
// 如果是个栈block,那我们的故事就开始了
// 首先当然是申请内存啦,之前的block占多大的内存,咱们就得占多大
struct Block_layout *result = malloc(aBlock->descriptor->size);
// 申请失败那自然拜拜咯
if (!result) return NULL;
// memmove()将之前栈上所有的数据迁移到刚刚申请的内存上
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 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.
// 最后将isa设置成_NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}

上述代码就是把block从栈区推入堆区的具体操作。
当然有retain就会有release。

那block是怎么销毁的?

依然是runtime.c文件中,找到_Block_release()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// API entry point to release a copied Block
void _Block_release(const void *arg) {
struct Block_layout *aBlock = (struct Block_layout *)arg;
// 依然以判断传入的block是否为空
if (!aBlock) return;
// 判断block是否是全局block
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
// 判断是不是堆block
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;

// 走到这里,证明没有异常
// 先加个锁,保证不被别的线程释放了
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
_Block_call_dispose_helper(aBlock);
// _Block_destructInstance好像没做啥事
_Block_destructInstance(aBlock);
// free
free(aBlock);
}
}

从栈到堆,然后到被释放其实也就这么些代码,实际上并不复杂。

CATALOG
  1. 1. block的结构
  2. 2. block为什么要用copy修饰?
  3. 3. block是怎么被推入堆区的?
  4. 4. 那block是怎么销毁的?