OC底层探索 - block

942 阅读8分钟

block的类型

block有三种类型: 栈block(NSStackBlock)堆block(NSMallocBlock)全局block(NSGlobalBlock)

void (^ block)(void) = ^{

};
NSLog(@"%@",block); // <__NSGlobalBlock__: 0x107b810e0>


int a = 1;
void (^__weak block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@",block); // <__NSStackBlock__: 0x7ff7be318f18>

int a = 1;
void (^ block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@",block); // <__NSMallocBlock__: 0x600001212490>

对于这三种类型的block的判断遵循2个原则:

  1. block如果没有使用外部变量,或者只使用静态变量和全局变量,那一定是 全局blcok
  2. block如果使用了外部变量,而且不是静态变量或全局变量,如果赋值给强引用的是 堆block,如果赋值给弱引用的是 栈blcok

block 本质

NSObject *objTest = [NSObject new];
void(^testBlock)(void) = ^{
    NSLog(@"%@", objTest);
};
NSObject *objTest = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
void(*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, objTest, 570425344));

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 捕获外部变量为自己的成员变量,所以block创建时就已经进行捕获了
  NSObject *objTest;
  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_objTest, int flags=0) : objTest(_objTest) {
    // 说明block在创建时,内存时分配在栈上的
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

从上面转换后的C++代码可知:

  • block的本质是一个结构体
  • block在创建时,内存是分配在栈上的
  • block在创建的时候,而不是使用时,就已经捕获外部变量

为什么block要用copy关键字修饰?

因为block在创建的时候,它的内存是分配在栈上的,而不是在堆上。

栈区的特点是: 对象随时有可能被销毁,一旦被销毁,在调用的时候,就会造成系统的崩溃。所以我们要使用 copy 把它拷⻉到堆上。

在ARC下, 对于block使用 copystrong 其实都一样, 因为block的 retain 就是用copy来实现的, 所以在ARC下 block使用 copy 和 strong 都可以。

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

// block结构体
struct Block_layout {
    // 指向block的三种类型
    void *isa;
    // block的附加信息
    volatile int32_t flags; // contains ref count
    // 保留的变量
    int32_t reserved;
    // libffi ->
    // block实现的函数指针
    BlockInvokeFunction invoke;
    // 存放copy、 dispose函数, block大小, block签名等信息
    // copy和dispose函数是用来对block内部的对象进行内存管理的,
    // block拷⻉到堆上会调用copy函数
    // 在block从堆上释放的时候会调用dispose函数。
    // block的签名:@?
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
// 拷贝 block,
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话);
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // 如果 arg 为 NULL,直接返回 NULL
    if (!arg) return NULL;
    // The following would be better done as a switch statement
    // 强转为 Block_layout 类型
    aBlock = (struct Block_layout *)arg;
    const char *signature = _Block_descriptor_3(aBlock)->signature;
    // 如果现在已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        // 就只将引用计数加 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        // block 现在在栈上,现在需要将其拷贝到堆上
        // 在堆上重新开辟一块和 aBlock 相同大小的内存
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        // 开辟失败,返回 NULL
        if (!result) return NULL;
        // 将 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;
#endif
        // reset refcount
        // 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
        result->flags |= BLOCK_NEEDS_FREE | 2// logical refcount 1
        // copy 方法中会调用做拷贝成员变量的工作
        _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 循环引用

既然block创建时就会捕获外部变量,那么如果外部变量本身持有block就会造成循环引用导致无法释放

image.png

无论我们在block中使用其持有类的属性,还是成员变量,都会出现循环引用。

属性我们可以理解,因为有self, 所以block捕获了self本身,但是成员变量为什么也会有循环引用呢?

image.png 通过c++代码可以确认,无论是使用了其持有类的属性,还是成员变量,block并不会逐个捕获使用到的变量,而是直接捕获了其持有类本身

那么block的循环引用问题该如何解决呢?

1. 使用weak

__weak typeof(self) weakSelf = self;
self.block = ^{
    NSLog(@"%@", weakSelf.name);
};

// 基于某些场景(比如异步耗时或者延时操作),我们也可以 weak strong 结合使用,能够保证 block 执行后 self 才释放
__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", strongSelf.name);
    });
};
self.block();

2.使用参数

typedef void(^ABlock)(ViewController *vc);
self.block = ^(ViewController *vc){
    NSLog(@"%@",vc.name);
};
self.block(self);

3.使用临时变量

// 加 __block 是因为 block 中要对 vc 置 nil
__block ViewController *vc = self;
self.block = ^{
    NSLog(@"%@",vc.name);
    // 这里一定要置nil,要不然还是会循环引用
    vc = nil;
};
self.block();

__block

我们知道在block内部可以修改全局变量和静态变量的值,但是不允许修改局部变量的值。要在block内部修改局部变量的值需要用 __block 修饰。

int a = 0;

int main(int argc, const char * argv[]) {
    static int b = 0;
    __block int c = 0;
    int d = 0;
    NSString *e = @"e";
    void(^block)(void) = ^{
        a ++;
        b ++;
        c ++;
        NSLog(@"%d, %d, %d, %d, %@", a, b, c, d, e);
    };
    block();//打印 1, 1, 1, 0, e
}

image.png

image.png

通过c++代码可知:

  • block 不会捕获全局变量
  • block 会以指针捕获静态变量的地址
  • __block修饰的变量会以 __Block_byref_XXX_X指针捕获
  • 基础类型普通变量直接捕获值,对象类型普通变量引用计数+1捕获
  • __block修饰的变量在 block 中使用时,不是直接取的值,而是通过c->__forwarding->c中转了一次的
// 1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3,
//    被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable,
//    原来 byref 的 forwarding 也会指向堆上的 byref;
// 2. 如果 byref 已经在堆上,就只增加一个引用计数。
// 参数 dest是一个二级指针,指向了目标指针,最终,目标指针会指向堆上的 byref
static struct Block_byref *_Block_byref_copy(const void *arg) {
    // arg 强转为 Block_byref * 类型
    struct Block_byref *src = (struct Block_byref *)arg;

    // 引用计数等于 0
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        // 为新的 byref 在堆中分配内存
        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
        // 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。
        // 为什么是 2 呢?注释说的是 non-GC one for caller, one for stack
        // one for caller 很好理解,那 one for stack 是为什么呢?
        // 看下面的代码中有一行 src->forwarding = copy。src 的 forwarding 也指向了 copy,相当于引用了 copy
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        // 堆上 byref 的 forwarding 指向自己
        copy->forwarding = copy; // patch heap copy to point to itself
        // 原来栈上的 byref 的 forwarding 现在也指向堆上的 byref
        src->forwarding = copy// patch stack to point to heap copy
        // 拷贝 size
        copy->size = src->size;

        // 如果 src 有 copy/dispose helper
        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
            // 取得 src 和 copy 的 Block_byref_2
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            // copy 的 copy/dispose helper 也与 src 保持一致
            // 因为是函数指针,估计也不是在栈上,所以不用担心被销毁
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            // 如果 src 有扩展布局,也拷贝扩展布局
            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);
                // 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上
                copy3->layout = src3->layout;
            }
            // 调用 copy helper,因为 src 和 copy 的 copy helper 是一样的,所以用谁的都行,调用的都是同一个函数
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            // 如果 src 没有 copy/dispose helper
            // 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    // src 已经在堆上,就只将引用计数加 1
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }

    return src->forwarding;
}


// 对 byref 对象做 release 操作,
// 堆上的 byref 需要 release,栈上的不需要 release,
// release 就是引用计数减 1,如果引用计数减到了 0,就将 byref 对象销毁
static void _Block_byref_release(const void *arg) {
    struct Block_byref *byref = (struct Block_byref *)arg;

    // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
    // 取得真正指向的 byref,如果 byref 已经被堆拷贝,则取得是堆上的 byref,否则是栈上的,栈上的不需要 release,也没有引用计数
    byref = byref->forwarding;

    // byref 被拷贝到堆上,需要 release
    if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
        // 取得引用计数
        int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
        os_assert(refcount);
        // 引用计数减 1,如果引用计数减到了 0,会返回 true,表示 byref 需要被销毁
        if (latching_decr_int_should_deallocate(&byref->flags)) {
            // 如果 byref 有 dispose helper,就先调用它的 dispose helper
            if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
                struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                // dispose helper 藏在 Block_byref_2 里
                (*byref2->byref_destroy)(byref);
            }
            free(byref);
        }
    }
}

结合源码研究可知,__block修饰的变量,在编译过后会变成 __Block_byref__XXX 类型的结构体,在结构体内部有一个 __forwarding 的结构体指针,指向结构体本身。 block创建的时候是在栈上的,在将栈block拷⻉到堆上的时候,同时也会将block中捕获的对象拷⻉到堆上,然后就会将栈上的 __block 修饰对象的 __forwarding 指针指向堆上的拷⻉之后的对象。 这样我们在block内部修改的时候虽然是修改堆上的对象的值,但是因为栈上的对象的 __forwarding 指针指向堆上的对象。因此就可以达到类似同时修改的目的。