iOS Block引用计数及内存管理

Block是在iOS4引入的新特性,是一种特殊的数据类型,今天我们就从源码层面探索一下Block具体是一种什么类型,并探寻下Block的内存管理方式。

一、Block类型

对于Block是什么类型,其实网上已经给出了答案,那就是Block实例也是一种对象。这个观点是完全正确的,我们可以从以下两个方面进行验证:

1. 源码

目前关于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_layoutisa指针可以得出以上结论。

2. clang转换后的代码

我们可以简单写一个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引用计数

既然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分类

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中类型,其中可以分为以下几种情况:

_NSConcreteGlobalBlock & _NSConcreteStackBlock

在官方文档中,有以下一段描述:

void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock

文档表明Block在创建时,只能创建出_NSConcreteStackBlock与_NSConcreteGlobalBlock两种类型的Block,那么如何创建这两种类型的Block,网上已经有不少文章指出了,关键点就在Block是否需要引用外部变量,若未引用,则为_NSConcreteGlobalBlock,若引用,则为_NSConcreteStackBlock。

_NSConcreteMallocBlock

在源码中,搜索_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位于堆上。

_NSConcreteAutoBlock & _NSConcreteFinalizingBlock

这两种类型的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本质上是一个对象,那么涉及到内存管理的方法就只有retainrelease以及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);

_NSConcreteGlobalBlock

我们创建一个全局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来说,调用retainreleasecopy来说,是无效的。

同时可以看出,全局Block的引用计数一直保持在0。

_NSConcreteStackBlock

我们创建一个栈Block,分别调用retainrelease(由于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来说,调用retainrelease是,也未触发相关方法,即对于栈Block来说,调用retainrelease来说,是无效的。

同时可以看出,此Block的地址较高,位于栈区,引用指数也一直保持在0,释放时机由系统控制。

_NSConcreteMallocBlock

我们由栈Block通过copy方法创建一个堆Block,并分别调用retainreleasecopy

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

由以上输出可知:

  1. 将栈Block通过copy方法复制到堆上时,调用的是_Block_copy方法,在copy之后,栈Block的引用计数还是0,堆Block的引用计数变为2;
  2. 为堆Block调用retain和copy方法,调用的均是_Block_copy方法,调用后,堆Block的引用计数增加2;
  3. 为堆Block调用release方法,调用的是_Block_release方法,调用后,堆Block的引用计数减少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中的。

你可能感兴趣的:(iOS开发)