本文主要在 MRC 和 ARC 环境下,通过实例来分析block
在内存中的存储位置,阅读本文的读者需要提前了解block
的相关知识和使用技巧。
我们先定义一个Block_t
类型:
typedef void (^Block_t)(void);
然后再定义一个不捕获任何外部变量的block
,尝试去打印这个block
isa 指针指向的类型:
Block_t t = ^{
NSLog(@"I'm a block.");
};
Class cls = object_getClass(t);
NSLog(@"%@", cls); // __NSGlobalBlock__
无论在 MRC 还是在 ARC 环境下,能得到的结果都是__NSGlobalBlock__
。这个__NSGlobalBlock__
是什么东西呢?使用以下方式打印cls
的各级父类:
Class superCls = class_getSuperclass(cls);
Class superSuperCls = class_getSuperclass(superCls);
Class rootCls = class_getSuperclass(superSuperCls);
NSLog(@"%@", superCls); // __NSGlobalBlock
NSLog(@"%@", superSuperCls); // NSBlock
NSLog(@"%@", rootCls); // NSObject
可以发现__NSGlobalBlock__
是NSBlock
的一个子类,而NSBlock
是block
基于Cocoa
的一层封装。在blcok
的实现层来看,LLVM 为其给出了如下的结构体形式:
struct Block_literal_1 {
void *isa; // 初始化为 &_NSConcreteStackBlock 或 &_NSConcreteGlobalBlock
int flags; // 标志位
int reserved; // 占位用
void (*invoke)(void *, ...); // block 的实现函数指针
struct Block_descriptor_1 { // block 的附加描述信息
...
} *descriptor;
// imported variables
};
这与我们使用clang -rewrite-objc
分析出来的源码有些不一致,但其内存布局基本相同(相关源码中结构体的名称叫Block_layout
)。由于block
也会被当做对象看待,该结构体中的isa
指针需要指向其所属类型,那么_NSConcreteStackBlock
就表明了block
的具体类型。在 libclosure 源码中还能找到其他类型的block
,block
的isa
指针始终指向下面这些指针数组的首地址,该指针也决定了block
的类名称。
BLOCK_EXPORT void * _NSConcreteMallocBlock[32];
BLOCK_EXPORT void * _NSConcreteAutoBlock[32];
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32];
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32];
// declared in Block.h
// BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
// BLOCK_EXPORT void * _NSConcreteStackBlock[32];
其中
_NSConcreteFinalizingBlock
,_NSConcreteWeakBlockVariable
和_NSConcreteAutoBlock
只在 GC 环境下使用,我对这个也不太了解,暂且不讨论。
因此,根据block
命名规则来看block
的存储域大致有 3 个地方:全局区(数据区域 .data 区)、栈区和堆区。
接下来,我们要根据各种实例分析block
的存储域,这里使用的打印block
的方式而非用clang -rewrite-objc
命令分析,原因是后者只是对源码的一种改写,并不能真正反映blcok
存储域的变化,block
的isa
指针在这种情况下永远只会被初始化成_NSConcreteStackBlock
或者_NSConcreteGlobalBlock
。
在上面的实例中,一个不捕获任何外部变量的block
被存放在全局区。关于在全局区的block
,我觉得还可以补充一点,block
作为全局变量并初始化时,无论是否捕获外部变量,在 MRC 和 ARC 环境下都会被存放在全局区,并且对处于全局区的block
进行 copy 操作是无效的(后面会解释到)。以下代码可以进行验证。
int a = 1;
Block_t t = ^{
NSLog(@"I'm a block.");
};
Block_t t1 = ^{
a = 2;
NSLog(@"I'm a block too.");
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%@", t); // <__NSGlobalBlock__: 0x100001050>
NSLog(@"%@", t1); // <__NSGlobalBlock__: 0x100001090>
NSLog(@"%@", [t1 copy]); // <__NSGlobalBlock__: 0x100001090>
}
return 0;
}
当然,还有 3 种情况下的block
会被放置在全局区中,这一部分文章后面会具体分析。
那么捕获了外部变量的block
会被存放在哪里?这个问题需要分几种情况具体分析。
我们先来看block
捕获了一个auto
局部变量的情况,代码如下:
{
int a = 1;
Block_t t = ^{
NSLog(@"I'm a block. %d", a);
};
NSLog(@"%@", t);
}
在 MRC 和 ARC 中分别打印如下:
MRC: <__NSStackBlock__: 0x7ffeefbff558>
ARC: <__NSMallocBlock__: 0x100443280>
在 MRC 环境中t
被存放在栈区,这个不难理解。除了之前提到过的全局区block
在初始化时会被放置在全局区(impl.isa = _NSConcreteGlobalBlock),在其他情况下定义并初始化的block
都会被放置在栈区(impl.isa = _NSConcreteStackBlock)。然而在 ARC 环境中t
并不在栈中,它被放置于堆区,这是我们第一个遇到的存放于堆区里的block
(impl.isa = _NSConcreteMallocBlock)。block
结构关于isa
的注释里明确表示isa
指针只会被初始化为_NSConcreteGlobalBlock
或_NSConcreteStackBlock
,那么_NSConcreteMallocBlock
一定是在运行时才存在的一种状态。libclosure 源码的runtime.c
文件中的_Block_copy
函数实现了更改block isa
指针的操作:
// 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.
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
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.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
以上代码表明了对一个block
执行 copy 操作需要进行哪些操作。第一个 if 很简单,如果入参为空则返回空即可。前面提到的blcok
结构体重有一个flags
标志位,这个成员变量记录着block
的状态和其引用计数,一个变量记录多种信息在 Apple 的源代码中很常见。在这个函数中,如果flags
标志位包含BLOCK_NEEDS_FREE
,表明该block
存在于堆中,因此所需要做的就是增加其引用计数,返回原地址即可。如果block
的标志位包含BLOCK_IS_GLOBAL
,说明其存在于全局区,直接返回原来的block
即可。最后一种情况就是block
在栈中,需要重新开辟一块内存空间将原来的block
的成员变量和函数地址全部复制到新内存空间,并重新设置其flags
,接着更改isa
指针类型为_NSConcreteMallocBlock
,最后返回新block
的首地址。
分析到这里可以发现,如果一个block
是一个堆 block
(这样称呼可能会比block
被存放在堆区更简洁好听一些),那么它可能是从栈上 copy 过来的。这真是句没用的废话,不过这能解释之前的疑问,为什么 ARC 环境下t
是一个堆 block
?原因是在 ARC 中,大多数情形下编译器会自动将block
copy 到堆中,也就是编译器自己帮我们 copy 一个block
的副本,我们使用的是它的副本,并不是原来的block
。
上面的例子还能引申出另外一种情况,如果block
同时捕获了auto
局部变量和全局变量,它又会在哪里,还和上面那个例子一样么?
int g_a = 1;
int main(int argc, const char * argv[])
{
int b = 2;
Block_t t = ^{
NSLog(@"a = %d, b = %d", g_a, b);
};
NSLog(@"%@", t);
return 0;
}
答案很简单,确实是一样的。MRC 中t
是栈 block
,ARC 中t
是堆 block
,它一定不会是全局 block
,因为在使用全局变量的地方不能使用auto
变量。
现在我想抛出两个问题。
第一,如果t
里只捕获了那个全局变量g_a
,t
会是什么类型的block
呢?
答:当然是全局 block
了。
第二,如果变量b
是静态局部变量(static int a = 2;)t
会是什么类型的block
呢?
答:依旧是全局 block
咯。静态变量和全局变量是都是放在全局区的嘛。
到目前为止,3 中类型的block
都出现过了,现在我们来总结一下:
Block 的类型 | 条件 |
---|---|
全局 block | block 被初始化为全局变量时;block 未捕获任何外部变量时;block 只捕获了全局/静态变量时。 |
栈 block | MRC 中block 捕获了auto 局部变量;ARC 中不存在 栈 block . |
堆 block | ARC 中block 捕获了auto 局部变量;对除 全局 block 外的block 执行 copy 操作。 |
前面我们谈论的都是block
在定义和初始化时的存储域,接下来我们继续分析block
在函数中作为形参和返回值的存储域,这一部分非常简单,如果你明白引用类型的参数传递和返回值的一些特点,这部分可以忽略不看了。
先来看看block
作为形参的存储域,其实这个没什么好说的。block
被当做 Objective-C 对象看待时,其是一个引用类型,其形参和实参是同一个首地址。
再来看block
作为返回值时的存储域。定义如下函数:
Block_t func(Block_t aBlock)
{
#if __has_feature(objc_arc)
return aBlock;
#else
return [aBlock autorelease];
#endif
}
我们调用func
函数时,将一个未捕获任何外部变量的block
作为该函数的参数:
Block_t t = ^{
NSLog(@"I am a block.");
};
NSLog(@"%@", t); // <__NSGlobalBlock__: 0x100001058>
Block_t t2 = func(t);
NSLog(@"%@", t2); // <__NSGlobalBlock__: 0x100001058>
在 ARC 和 MRC 环境下发现t
和t2
同为全局 block
,并且内存地址一致,也就是说全局 block
作为返回值时,它的存储域并不会变化。这一点很好理解,全局 block
不依赖任何外部条件,它可以看做为字面量,其内存地址是唯一确定和共享的。
接下来,将一个捕获auto
局部变量的block
作为该函数的参数:
int a = 1;
Block_t t = ^{
NSLog(@"a = %d", a);
};
NSLog(@"%@", t);
// ARC: <__NSMallocBlock__: 0x10060ef10>
// MRC: <__NSStackBlock__: 0x7ffeefbff558>
Block_t t2 = func(t);
NSLog(@"%@", t2);
// ARC: <__NSMallocBlock__: 0x10060ef10>
// MRC: <__NSStackBlock__: 0x7ffeefbff558>
结果还是一样,返回的block
和原来的block
始终是同一个。还有,综合上面的这些例子我们可以发现 ARC 中已经不存在了栈 block
,在编译期间栈 block
已被转移至堆区。
那么最后现在我们来装模作样得总结一下block
在函数中的存储域变化:
原 Block 的类型 | 作为形参和返回值 |
---|---|
全局 block | 与原block 一致,为其本身。 |
栈 block | 与原block 一致,为其本身。 |
堆 block | 与原block 一致,为其本身。 |