iOS底层探索 - Block

348 阅读9分钟

block是我们开发经常遇到的一个结构,本篇我们就来探索一下它的结构。

block的分类

block的分类相信大家已经很清楚了,分为全局block、堆block和栈block。我们来个例子看看它们的区别

  • 全局block

        void (^block)(void) = ^{
        };
        NSLog(@"%@",block);
        ///打印结果
        <__NSGlobalBlock__: 0x1072d4100>
    

    全局block是指不捕获任何外部变量的block,只会使用静态变量和全局变量,存储于内存的全局区

  • 堆block

        int a = 10;
        void (^block)(void) = ^{
            NSLog(@"Cooci - %d",a);
        };
        NSLog(@"%@",block);
        ///打印结果
        <__NSMallocBlock__: 0x60000130c4b0>
    

    堆block会捕获外部变量,存储于内存的堆区

  • 栈block

        int a = 10;
        void (^__weak block)(void) = ^{
            NSLog(@"Cooci - %d",a);
        };
        NSLog(@"%@",block);
        ///打印结果
        <__NSStackBlock__: 0x7ffee06ac4d8>
    

    栈block也会捕获外部变量,和堆block的区别是需要加__weak修饰,它存储于内存的栈区

block的循环引用

block引起循环引用的原因

A、B相互持有,所以导致A无法调用dealloc方法给B发送release信号,而B也无法接收到release信号。所以A、B此时都无法释放。如图所示

循环引用.jpg

解决循环引用

我们看一段循环引用的代码

NSString *name = @"JS";
self.block = ^(void){
    NSLog(@"%@",self.name);
};
self.block();

这段代码出现了循环引用,因为在block内部使用了外部变量name,导致block持有了self,而self原本是持有block的,所以导致了self和block的相互持有

解决方法:

  • __weak和__strong组合使用

    typedef void(^JSBlock)(void);
    ​
    @property(nonatomic, copy) JSBlock jslBlock;
    ​
    __weak typeof(self) weakSelf = self;
    self.jslBlock = ^(void){
          __strong typeof(weakSelf) strongSelf = weakSelf;
         NSLog(@"%@",weakSelf.name);
    }
    self.jslBlock();
    

    这是我们最容易想到的方式,使用__weak打破强引用,__strong的作用的方式self提前释放,而block执行的时候因为self已经释放而拿不到值。

  • __block定义一个临时变量指向self。

    __block ViewController *vc = self;
    self.jslBlock = ^(void){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
            vc = nil;//需手动释放
        });
    };
    self.jslBlock();
    

    这种方式是在方法外部定义一个指向self的变量,block内部捕获临时变量,使用结束后将临时变量置为nil,加__block的原因是需要在block内存对其进行置空操作。

  • block加一个参数,使用参数

    typedef void(^JSBlock)(ViewController *);
    @property(nonatomic, copy) JSBlock jslBlock;
    self.jslBlock = ^(ViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    self.jslBlock(self);
    

Block的底层分析

我们主要通过clang和断点调试的方式分析。

xcrun编译分析

我们首先自定义一个block.c文件

#include "stdio.h"
int main(){
    int a = 18;
    void(^block)(void) = ^{
        printf("js - %d",a);
    };
    block();
    return 0;
}

使用xcrun命令讲block.c编译成block.cppxcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
        printf("js - %d",a);
    }
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 a = 18;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
​
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

我们把类型强转的代码去掉:

int main(){
  int a = 18;
    void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a));
    block->FuncPtr(block);
    return 0;
}

简化之后我们看到block代码块是一个__main_block_impl_0__main_block_impl_0的结构是一个结构体,它的impl也是一个结构体

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
///
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

注意:在结构体内部捕获到了外部变量a,且在结构体内部生成了一个成员变量a与其对应。

我们对代码做一下修改:

int main(){
    __block int a = 18;
    void(^block)(void) = ^{
        a++;
        printf("js - %d",a);
    };
    block();
    return 0;
}

重新xcrun一下看看结果:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  • 可以看到,现在的a和之前的a不同的是加了__Block_byref_a_0 *修饰,这样就可以对捕获到的变量进行修改,传给block是a的地址,所以block内部可以修改。
  • impl.isa = &_NSConcreteStackBlock说明现在栈类型根据我们前面的分析这里应该是堆block,为什么不同呢。
  • fp是一个函数式保存,如果不调用不会执行。

源码分析

我们首先用汇编,查看源码在哪个库中,我们打断点

断点.jpg 然后看汇编代码,

源码定位.jpg 我们添加符号断点objc_retainBlock:

1629984483869.jpg 所以我们去libobjc去搜索objc_retainBlock

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

实际调用了_Block_copy,在libobjc库中并没有找到方法的实现,我们继续打符号断点:

1629984996709.jpg _Block_copy函数的实现在libsystem库中,这个库没有开源,我们找一个替换的库libclosure的源码分析。我们在libclosure源码中搜索_Block_copy:

// 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.
        size_t size = Block_size(aBlock);
        struct Block_layout *result = (struct Block_layout *)malloc(size);
        if (!result) return NULL;
        memmove(result, aBlock, size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
​
#if __has_feature(ptrauth_signed_block_descriptors)
        if (aBlock->flags & BLOCK_SMALL_DESCRIPTOR) {
            uintptr_t oldDesc = ptrauth_blend_discriminator(
                    &aBlock->descriptor,
                    _Block_descriptor_ptrauth_discriminator);
            uintptr_t newDesc = ptrauth_blend_discriminator(
                    &result->descriptor,
                    _Block_descriptor_ptrauth_discriminator);
​
            result->descriptor =
                    ptrauth_auth_and_resign(aBlock->descriptor,
                                            ptrauth_key_asda, oldDesc,
                                            ptrauth_key_asda, newDesc);
        }
#endif
#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;
    }
}

其中Block_layout结构体的结构是:

struct Block_layout {
    void * __ptrauth_objc_isa_pointer isa;//isa 标识是栈、堆block类型
    volatile int32_t flags; // contains ref count 引用计数
    int32_t reserved;//流程数据
    BlockInvokeFunction invoke;//调用函数
    struct Block_descriptor_1 *descriptor;//相关描述
    // imported variables
};

对结构有个简单了解之后,我们打符号断点,看运行中block的结构。

  • objc_retainBlock

1629986289286.jpg 发现此时的block类型还是StackBlock

  • _Block_copy最后打一个断点:

1629986567227.jpg 此时block的类型就是__NSMallocBlock__类型了。

blockLayout结构
struct Block_layout {
    void * __ptrauth_objc_isa_pointer isa;//isa 标识是栈、堆block类型
    volatile int32_t flags; // contains ref count 引用计数
    int32_t reserved;//流程数据
    BlockInvokeFunction invoke;//调用函数
    struct Block_descriptor_1 *descriptor;//相关描述
    // imported variables
};

我们看Block_descriptor_1的结构,发现并没有上面调试打印的signature信息。

我们看源码发现:

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;//拷贝函数指针
    BlockDisposeFunction dispose;
};
​
#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 布局
}

Block_descriptor_2Block_descriptor_3是可选的,它们是通过Block_descriptor_1内存平移得到的。

捕获变量的copy
_Block_copy
// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
//  栈Block -> 堆Block
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;//强转为Block_layout类型对象,防止对外界造成影响
    if (aBlock->flags & BLOCK_NEEDS_FREE) {//是否需要释放
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {//如果是全局block,直接返回
        return aBlock;
    }
    else {//为栈block 或者 堆block,由于堆区需要申请内存,所以只可能是栈区
        // Its a stack block.  Make a copy. 它是一个堆栈块block,拷贝。
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);//申请空间并接收
        if (!result) return NULL;
        //通过memmove内存拷贝,将 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;//可以直接调起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;//设置block对象类型为堆区block
        return result;
    }
}

_Block_copy主要是将block从栈区拷贝到堆区

  • 如果需要释放,则直接释放

  • 如果是globalBlock不需要copy,返回

  • 剩下两种情况:堆区block和栈区block。由于堆区block需要申请内存,这里到这里只能是栈block。

    • 通过malloc申请内存空间用于接收block
    • 通过remove将block拷贝至新申请的内存中
    • 设置block对象的类型为堆区block。将isa指向__NSConcreteMallocBlock
_Block_object_assign

先看一个枚举的定义:

// Block 捕获的外界变量的种类
// Runtime support functions used by compiler when generating copy/dispose helpers
// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
    // see function implementation for a more complete description of these fields and combinations
    //普通对象,即没有其他的引用类型
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    //block类型作为变量
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    //经过__block修饰的变量
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    //weak 弱引用变量
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    //返回的调用对象 - 处理block_byref内部对象内存会加的一个额外标记,配合flags一起使用
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};

其中用的最多的是BLOCK_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF

static struct Block_byref *_Block_byref_copy(const void *arg) {
    //强转为Block_byref结构体类型,保存一份
    struct Block_byref *src = (struct Block_byref *)arg;
​
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack 申请内存
        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
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        //block内部持有的Block_byref 和 外界的Block_byref 所持有的对象是同一个,这也是为什么__block修饰的变量具有修改能力
        //copy 和 scr 的地址指针达到了完美的同一份拷贝,目前只有持有能力
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;
        //如果有copy能力
        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
            //Block_byref_2是结构体,__block修饰的可能是对象,对象通过byref_keep保存,在合适的时机进行调用
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;
​
            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);
                copy3->layout = src3->layout;
            }
            //等价于 __Block_byref_id_object_copy
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    return src->forwarding;
}

_Block_object_assign是在底层编译代码中,外部变量拷贝时调用的方法就是它。

  • 如果是普通对象,交给系统arc处理,拷贝对象指针,引用技术+1,外界变量不能释放。
  • 如果是block类型的变量,通过_Block_copy操作,将block从栈区拷贝到堆区。
  • 如果是__block修饰的变量,调用_Block_byref_copy函数,进行内存拷贝以及常规处理。
_Block_byref_copy
static struct Block_byref *_Block_byref_copy(const void *arg) {
    //强转为Block_byref结构体类型,保存一份
    struct Block_byref *src = (struct Block_byref *)arg;
​
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack 申请内存
        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
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        //block内部持有的Block_byref 和 外界的Block_byref 所持有的对象是同一个,这也是为什么__block修饰的变量具有修改能力
        //copy 和 scr 的地址指针达到了完美的同一份拷贝,目前只有持有能力
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;
        //如果有copy能力
        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
            //Block_byref_2是结构体,__block修饰的可能是对象,对象通过byref_keep保存,在合适的时机进行调用
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;
​
            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);
                copy3->layout = src3->layout;
            }
            //等价于 __Block_byref_id_object_copy
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    return src->forwarding;
}
  • 将传入的对象,强转为Block_byref结构体类型对象。保存
  • 如果没有将变量拷贝到堆上,就申请内存进行拷贝
  • 如果已经拷贝,则进行处理并返回
  • 其中copy和src的forwarding指针都是指向同一片内存。这就是为什么__block修饰的对象具有修改的能力。

三层copy小结

  • 第一层:通过_Block_copy实现对象的自身拷贝,从栈区拷贝至堆区
  • 第二层:通过_Block_byref_copy方法,将对象拷贝为Block_byref结构体类型
  • 第三次:调用_Block_object_assign方法,对__block修饰的当前变量的拷贝