Block是在iOS4引入的新特性,是一种特殊的数据类型,今天我们就从源码层面探索一下Block具体是一种什么类型,并探寻下Block的内存管理方式。
对于Block是什么类型,其实网上已经给出了答案,那就是Block实例也是一种对象。这个观点是完全正确的,我们可以从以下两个方面进行验证:
目前关于Block的源码是公开的,具体下载位置为地址。
对于Block,本质上是一个结构体,其内容如下:
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
#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
};
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};
从Block_layout
的isa
指针可以得出以上结论。
我们可以简单写一个Block:
void(^block1)(void) = ^(void) {
NSLog(@"block1");
};
通过clang -rewrite-objc main.m
得到转换后的代码,其中与该Block有关的内容如下:
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) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hb_tnc4751s73b8_zwpzmxttpvm0000gn_T_main_f377bb_mi_0);
}
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)};
void(*block1)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
可以看到该Block本身就是一个__main_block_impl_0
的结构体,而该结构体中第一个成员便是struct __block_impl
:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
可以看到,虽然转换出来的代码最终是__block_impl
结构体,但他与源码的结构体内容完全一致,且第一个成员为isa指针,同样可以证明Block变量就是一个对象。
既然Block也是一种对象,那它是否也遵循对象的引用计数方式呢?
我们从runtime的SideTable源码并没有找到任何关于Block引用计数的相关代码,但这并不表示Block和引用计数没有关联,Block与引用计数的关联其实很简单,就是保持在struct Block_layout
结构体本身中:
volatile int32_t flags; // contains ref count
flags
成员的注释中表明了它保持了引用计数的内容,那么他是如何保存的呢?
在struct Block_layout
声明的代码上部就有一些关于flags
标志的定义:
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
结合这些标志,我们可以得出flags
成员的含义:
flags二进制下标 |
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 ... 1 | 0 |
代表含义 |
是否有扩展布局 | 是否有签名 | 返回值是否在栈上 | 是否是全局Block | 是否采用垃圾回收机制 | helper是否有C++代码 | 是否有copy/dispose方法 | 是否需要释放,即存在与堆上 | 引用计数 | 是否正在释放 |
由以上源码可知,Block的引用计数保存在32位flags
的第1-23位上,同时flags
也指定了Block是否需要释放
等信息。
既然有是否需要释放
这个信息,那么肯定就存在Block
需要释放与不需要释放两种情况,那么这两种情况是如何出现的,我们需要从Block的分类开始说起。
Block既然有isa指针,那么Block属于哪种对象呢?
从源码中我们可以得到以下分类:
void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
Block共有以上5中类型,其中可以分为以下几种情况:
在官方文档中,有以下一段描述:
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
文档表明Block在创建时,只能创建出_NSConcreteStackBlock与_NSConcreteGlobalBlock两种类型的Block,那么如何创建这两种类型的Block,网上已经有不少文章指出了,关键点就在Block是否需要引用外部变量,若未引用,则为_NSConcreteGlobalBlock,若引用,则为_NSConcreteStackBlock。
在源码中,搜索_NSConcreteMallocBlock,发现只有一处代码有isa = _NSConcreteMallocBlock,且代码位于_Block_copy中:
//libclosure-74
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 =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
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;
#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;
}
}
由源码可知,当_NSConcreteStackBlock调用copy方法的时候,才会变为_NSConcreteMallocBlock。且这是创建_NSConcreteMallocBlock的唯一途径。
同时可以看出,在创建_NSConcreteMallocBlock类型的Block时,是通过malloc方法申请的堆内存,说明该类型的Block位于堆上。
这两种类型的Block创建方式在源码中同样只有一处,在_Block_copy_internal中:
//libclosure-63
static void *_Block_copy_internal(const void *arg, const bool wantsOne) {
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_GC) {
// GC refcounting is expensive so do most refcounting here.
if (wantsOne && ((latching_incr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK) == 2)) {
// Tell collector to hang on this - it will bump the GC refcount version
_Block_setHasRefcount(aBlock, true);
}
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// Its a stack block. Make a copy.
if (!isGC) {
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
result->isa = _NSConcreteMallocBlock;
_Block_call_copy_helper(result, aBlock);
return result;
}
else {
// Under GC want allocation with refcount 1 so we ask for "true" if wantsOne
// This allows the copy helper routines to make non-refcounted block copies under GC
int32_t flags = aBlock->flags;
bool hasCTOR = (flags & BLOCK_HAS_CTOR) != 0;
struct Block_layout *result = _Block_allocator(aBlock->descriptor->size, wantsOne, hasCTOR || _Block_has_layout(aBlock));
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
// if we copy a malloc block to a GC block then we need to clear NEEDS_FREE.
flags &= ~(BLOCK_NEEDS_FREE|BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
if (wantsOne)
flags |= BLOCK_IS_GC | 2;
else
flags |= BLOCK_IS_GC;
result->flags = flags;
_Block_call_copy_helper(result, aBlock);
if (hasCTOR) {
result->isa = _NSConcreteFinalizingBlock;
}
else {
result->isa = _NSConcreteAutoBlock;
}
return result;
}
}
由源码可知,在使用垃圾回收机制时才会创建这两种类型的Block,且当涉及C++代码的helper会生成_NSConcreteFinalizingBlock类型的Block,其他情况下生成_NSConcreteAutoBlock类型的Block。
这两种类型的Block我们可以不用关注了,在iOS上并没有垃圾回收机制,同时在最新的libclosure代码中,这两种类型的Block已经没有创建路径了,即不可能创建出这两种类型的Block了。
为了能够直观的观察Block引用计数的变化,我们模拟构造与源码布局一致的结构体,这样便于直接观察flags
的变化:
enum {
MBlock_REFCOUNT_MASK = (0xfffe),
MBlock_HAS_COPY_DISPOSE = (1 << 25),
MBlock_HAS_CTOR = (1 << 26),
MBlock_IS_GLOBAL = (1 << 28),
MBlock_HAS_STRET = (1 << 29),
MBlock_HAS_SIGNATURE = (1 << 30),
};
struct MBlockLiteral {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct MBlockDecriptor1 *descriptor;
};
struct MBlockDescriptor1 {
uintptr_t reserved;
uintptr_t size;
};
同时我们知道Block本质上是一个对象,那么涉及到内存管理的方法就只有retain
、release
以及copy
三个方法了,我们接下来就从这三个方法进行研究。
从源码中,我们可以找寻到这三个方法对应的原始方法,分别为:bool _Block_tryRetain(const void *arg)
、void _Block_release(const void *arg)
以及void *_Block_copy(const void *arg)
。
为了方便观察三个方法的调用情况,我们使用fishhook对这三个方法进行hook:
static bool hook_Block_tryRetain(const void *arg);
bool (*origin_Block_tryRetain)(const void *arg);
static void * hook_Block_copy(const void *arg);
void *(*origin_Block_copy)(const void *arg);
static void hook_Block_release(const void *arg);
void (*origin_Block_release)(const void *arg);
static bool hook_Block_tryRetain(const void *arg) {
NSLog(@"Block_tryRetain %@", (__bridge id)arg);
return origin_Block_tryRetain(arg);
}
static void * hook_Block_copy(const void *arg) {
NSLog(@"Block_copy %@", (__bridge id)arg);
return origin_Block_copy(arg);
}
static void hook_Block_release(const void *arg) {
NSLog(@"Block_release %@", (__bridge id)arg);
origin_Block_release(arg);
}
struct rebinding rebindBlock_tryRetain;
rebindBlock_tryRetain.name = "_Block_tryRetain";
rebindBlock_tryRetain.replacement = hook_Block_tryRetain;
rebindBlock_tryRetain.replaced = (void *)&origin_Block_tryRetain;
struct rebinding rebindBlock_copy;
rebindBlock_copy.name = "_Block_copy";
rebindBlock_copy.replacement = hook_Block_copy;
rebindBlock_copy.replaced = (void *)&origin_Block_copy;
struct rebinding rebindBlock_release;
rebindBlock_release.name = "_Block_release";
rebindBlock_release.replacement = hook_Block_release;
rebindBlock_release.replaced = (void *)&origin_Block_release;
struct rebinding rebs[3] = {rebindBlock_tryRetain, rebindBlock_copy, rebindBlock_release};
rebind_symbols(rebs, 3);
我们创建一个全局Block,分别调用三个内存管理方法,然后观察输入日志:
void(^block1)(void) = ^(void) {
NSLog(@"block1");
};
struct MBlockLiteral *block1Ref = (__bridge struct MBlockLiteral *)block1;
NSLog(@"block1---%@ %lu", block1, (unsigned long)block1Ref->flags & MBlock_REFCOUNT_MASK);
[block1 retain];
NSLog(@"block1---%@ %lu", block1, (unsigned long)block1Ref->flags & MBlock_REFCOUNT_MASK);
[block1 copy];
NSLog(@"block1---%@ %lu", block1, (unsigned long)block1Ref->flags & MBlock_REFCOUNT_MASK);
[block1 release];
NSLog(@"block1---%@ %lu", block1, (unsigned long)block1Ref->flags & MBlock_REFCOUNT_MASK);
输出为:
block1---<__NSGlobalBlock__: 0x106132050> 0
block1---<__NSGlobalBlock__: 0x106132050> 0
block1---<__NSGlobalBlock__: 0x106132050> 0
block1---<__NSGlobalBlock__: 0x106132050> 0
可以看出,对于全局Block来说,在调用三个内存管理方法时,并未触发相关方法,即对于全局Block来说,调用retain
、release
、copy
来说,是无效的。
同时可以看出,全局Block的引用计数一直保持在0。
我们创建一个栈Block,分别调用retain
和release
(由于copy方法会产生堆Block,我们将copy放在堆Block中讨论)。
int a = 0;
void(^block2)(void) = ^(void) {
NSLog(@"block2 %d", a);
};
struct MBlockLiteral *block2Ref = (__bridge struct MBlockLiteral *)block2;
NSLog(@"block2---%@ %lu", block2, (unsigned long)block2Ref->flags & MBlock_REFCOUNT_MASK);
[block2 retain];
NSLog(@"block2---%@ %lu", block2, (unsigned long)block2Ref->flags & MBlock_REFCOUNT_MASK);
[block2 release];
NSLog(@"block2---%@ %lu", block2, (unsigned long)block2Ref->flags & MBlock_REFCOUNT_MASK);
输出为:
block2---<__NSStackBlock__: 0x7ffeef0de0a0> 0
block2---<__NSStackBlock__: 0x7ffeef0de0a0> 0
block2---<__NSStackBlock__: 0x7ffeef0de0a0> 0
可以看出,对于栈Block来说,调用retain
和release
是,也未触发相关方法,即对于栈Block来说,调用retain
和release
来说,是无效的。
同时可以看出,此Block的地址较高,位于栈区,引用指数也一直保持在0,释放时机由系统控制。
我们由栈Block通过copy方法创建一个堆Block,并分别调用retain
、release
和copy
。
void(^block3)(void) = ^(void) {
NSLog(@"block3 %d", a);
};
block3 = [block3 copy];
struct MBlockLiteral *block3Ref = (__bridge struct MBlockLiteral *)block3;
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
[block3 retain];
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
[block3 copy];
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
[block3 release];
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
[block3 release];
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
输出为:
Block_copy <__NSStackBlock__: 0x7ffee4514068>
2020-06-17 16:20:28.750074+0800 TheTestTest[26212:315120] block3---<__NSStackBlock__: 0x7ffee4514068> 0
2020-06-17 16:20:28.750142+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 2
2020-06-17 16:20:28.750201+0800 TheTestTest[26212:315120] Block_copy <__NSMallocBlock__: 0x600001122340>
2020-06-17 16:20:28.750453+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 4
2020-06-17 16:20:28.750522+0800 TheTestTest[26212:315120] Block_copy <__NSMallocBlock__: 0x600001122340>
2020-06-17 16:20:28.750585+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 6
2020-06-17 16:20:28.750648+0800 TheTestTest[26212:315120] Block_release <__NSMallocBlock__: 0x600001122340>
2020-06-17 16:20:28.750713+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 4
2020-06-17 16:20:28.750778+0800 TheTestTest[26212:315120] Block_release <__NSMallocBlock__: 0x600001122340>
2020-06-17 16:20:28.750838+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 2
由以上输出可知:
通过以上结论,我们可知,堆Block的内存管理集中在_Block_copy
和_Block_release
两个方法中,我们来看一下两个方法的实现,其中的逻辑我已做出注释:
//libclosure-74
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// 若传入空,直接返回
if (!arg) return NULL;
aBlock = (struct Block_layout *)arg;
// 若Block的BLOCK_NEEDS_FREE标志为1,表示Block为堆Block,则调用latching_incr_int方法
if (aBlock->flags & BLOCK_NEEDS_FREE) {
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 若Block是全局Block,直接返回
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// 若Block是栈Block,则拷贝至堆区
// 在堆区申请空间
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
// 拷贝内容
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// 设置回调指针
result->invoke = aBlock->invoke;
#endif
// 将flag的1-23位引用计数标志位以及第0位是否正在释放位重置
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);
// 设置flag的BLOCK_NEEDS_FREE位置为1,表示此时处于堆区;同时与2,将第1位置为1,表示引用计数此时为1
result->flags |= BLOCK_NEEDS_FREE | 2;
// 调用copy helper
_Block_call_copy_helper(result, aBlock);
// 设置Block的isa指针为堆Block
result->isa = _NSConcreteMallocBlock;
return result;
}
}
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
// 若flags的第1-23位全部为1(即引用计数到达最大值)时,不做操作,直接返回
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
// 将flags的值增加2,即从第1位开始增加1,表示引用计数加1
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}
//libclosure-74
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不是堆Block,即是栈Block,直接返回
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
// 调用latching_decr_int_should_deallocate减少引用计数
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
// 调用dispose helper
_Block_call_dispose_helper(aBlock);
// 释放堆区内存
_Block_destructInstance(aBlock);
free(aBlock);
}
}
static bool latching_decr_int_should_deallocate(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
// 若flags的第1-23位全部为1(即引用计数到达最大值)时,不做操作,直接返回
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return false;
}
// 若flags的第1-23位全部为0(即引用计数为0)时,不做操作,直接返回
if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
return false;
}
// 将flags的值减少2,即从第1位开始减少1,表示引用计数减1
int32_t new_value = old_value - 2;
bool result = false;
// 若此时引用计数刚好是2,即计数为1,将flags减为1,此时第1位为0,第0位置为1,表示引用计数为0并正在释放
if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) {
new_value = old_value - 1;
result = true;
}
if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
return result;
}
}
}
源码逻辑并不复杂,在_Block_copy
中,有对全局Block的空处理,在_Block_release
中,有对全局Block和栈Block的空处理,从之前的分析我们可知,这些处理其实永远不用执行,但源码同样做了异常保护。
在每次增加/减少引用计数时,是将flags增加/减少2,这是由于flags的第1-23位保存的是引用计数,需要对第0位进行偏移。
以上就是Block引用计数机制及各个分类Block内存管理的实现原理,底部实现其实非常有设计性,使用一个32位的flags可以记录Block的各种信息,进而减少内存的使用。
如有兴趣,可以去研究一下Object的引用计数机制,它同样也是采用一个flag记录多个信息,其中引用计数是从第2位开始的,所以每次都是以4为单位进行增减的,但不一样的是,它的引用计数并没有保存在自身结构体中,而是保存在SideTable中的。