iOS-底层原理分析之Block本质

2,721 阅读8分钟

前言

写本篇文章的目的就是要搞明白以下几个问题?

  • block的底层实现原理
  • block的几种类型?
  • __block的作用是什么?有什么使用注意点?
  • block的循环引用问题解决?

什么是Block

Block是将函数及其上下文封装起来的对象。

Block的底层实现原理

Block的底层实现是一个结构体

void blockTest()
{
    void (^block)(void) = ^{
        NSLog(@"Hello");
    };

    block();
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        blockTest();
    }
}

可以通过clang命令查看编译器是如何实现Block的,进入main.m的目录,在终端输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m,然后会在当前目录生成main.cpp的C++文件,代码如下:

///Block的C++实现
struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
    ///构造函数
  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __block_impl {
   ///isa指针,指向一个类对象,有三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock和_NSConcreteMallocBlock
  void *isa;
    ///block的负载信息(引用计数和类型信息),按位存储
  int Flags;
    ///保留变量
  int Reserved;
    ///指向Block执行时调用的函数,也就是Block需要执行的代码块
  void *FuncPtr;
};

///Block 执行时调用的函数
static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_59_c8zx7n553c34b8d791v8l7ww0000gn_T_main_ab6df5_mi_0);
    }

static struct __blockTest_block_desc_0 {
    ///Block版本升级所需要的预留区空间
  size_t reserved;
    ///Block的大小=sizeof(struct __blockTest_block_impl_0)
  size_t Block_size;
} __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};

void blockTest() {
    ///block变成了一个指针,指向一个通过__blockTest_block_impl_0构造函数实例化的结构体实例
    ///__blockTest_block_func_0表示Block块的函数指针
    ///__blockTest_block_desc_0_DATA作为静态全局变量初始化__main_block_desc_0的结构体实例指针
    void(*block)(void) = ((void (*)())&__blockTest_block_impl_0((void *)__blockTest_block_func_0, &__blockTest_block_desc_0_DATA));
    ///调用Block
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        blockTest();
    }
}

画个图来表示各个结构体之间的关系就是:

截屏2022-06-12 14.30.59.png

Block的底层数据结构也可以用一张图表示:

截屏2022-06-26 14.39.40.png

探索Block的变量捕获

Block根据其类型可以分为三类:全局区Block、栈区Block、堆区Block。然而通过上面Block的C++底层实现,可以看到__block_impl有一个属性isa,而这个isa指向的对象有三种类型,也就是这三种Block。通过这个也可以看出,Block的也是一个对象。

截获auto变量值

void blockTest()
{
    int age = 20;
    void (^block)(void) = ^{
        NSLog(@"Hello==%d", age);
    };
    block();
}

通过Clang指令生成C++代码:

struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  int age;

  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到__blockTest_block_impl_0多了一个成员变量age,并且构造函数也多了一个参数age,传的仅仅是a ge的值。可以得出Block捕获的值,所以要想在Block内部修改局部变量的值是不行的。

使用static修饰变量

把上面的age改成使用static修饰

void blockTest()
{
    static int age = 20;
    void (^block)(void) = ^{
        NSLog(@"Hello==%d", age);
    };
    block();
}

通过Clang指令生成C++代码:

struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  int *age;
  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看出捕获的不再是变量的值,而是变量的指针地址,所以也可以在Block内部修改age的值。并且static修饰的局部变量叫作静态局部变量,是存储在静态存储区,这块内存只有在程序结束才会销毁,但是只是在声明它的代码块可见,所以传入变量的指针也不用担心变量销毁的问题。

全局变量

int age = 20;
void blockTest()
{
    void (^block)(void) = ^{
        NSLog(@"Hello==%d", age);
    };
    block();
}

通过Clang指令生成C++代码:

struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看出并没有把全局变量age捕获,是直接访问全局变量。

进而也可以得出如下结论:

截屏2022-06-12 14.22.43.png

__block修饰变量

如果想修改局部变量的值,可以通过__block修饰实现。

void blockTest()
{
    __block int age = 20;
    void (^block)(void) = ^{
        age = 26;
        NSLog(@"Hello==%d", age);
    };
    block();
}

通过Clang指令生成C++代码:

截屏2022-06-12 15.31.40.png

__blockTest_block_impl_0多出来一个成员变量__Block_byref_age_0 *age,我们看到经过__block修饰的变量类型变成了结构体__Block_byref_age_0,block捕获的是__Block_byref_age_0类型指针。调用函数的时候先通过__forwarding找到age指针,然后去取出age值。

并且可以看到这次的C++代码多出了两个函数:

截屏2022-06-12 15.41.21.png

__blockTest_block_copy_0中调用的是_Block_object_assign__blockTest_block_dispose_0中调用的是_Block_object_dispose

截屏2022-06-12 15.42.43.png

并且这个两个函数都有个参数8,看注释说是这个枚举值BLOCK_FIELD_IS_BYREF,在Block_private.h 中可以查看到:

截屏2022-06-12 15.46.43.png

这些枚举值表示的含义分别为:

  • BLOCK_FIELD_IS_OBJECT:OC对象类型
  • BLOCK_FIELD_IS_BLOCK:是一个block
  • BLOCK_FIELD_IS_BYREF:在栈上被__block修饰的变量
  • BLOCK_FIELD_IS_WEAK:被__weak修饰的变量,只在Block_byref管理内部对象内存时使用
  • BLOCK_BYREF_CALLER:处理Block_byref内部对象内存的时候会加的一个额外标记(告诉内部实现不要进行retain或者copy)

Block copy流程

// 拷贝 block
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
// 运行?stack -》malloc
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_copy源码中,从栈区copy到堆区的过程中,_Block_call_copy_helper(result, aBlock)的调用时为了复制栈区的Block里面的成员变量,给堆区的Block。其实最终会发现调用的是这个函数_Block_object_assign,根据参数 flags 的类型(对象、block、byref...),做了不同的处理

  • 对象类型,增加对象的引用计数;
  • block类型,会对该block执行一次block_copy操作;
  • __block修饰,会调用_Block_byref_copy;

__blockBlock类似,如果在栈区,会重新malloc一份,进行深拷贝操作,但这两个的forwarding都会指向堆区的,如果已经在堆区,只会将其引用计数+1。上面也有提到关于__block修饰变量。

Block_release 流程

void _Block_release(const void *arg) {
    // 1. 将指针转换为Block_layout结构
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;
    // 2. 如果是全局block,那么直接返回
    if (aBlock->flags & BLOCK_IS_GLOBAL) return;
    // 3. 如果不是堆block,那么直接返回
    if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
    // 4. 处理堆block的引用计数
    if (latching_decr_int_should_deallocate(&aBlock->flags)) {
        // 5. 释放block捕获的变量
        _Block_call_dispose_helper(aBlock);
        _Block_destructInstance(aBlock);
        // 6. 释放堆block
        free(aBlock);
    }
}

Block的内存管理

之前提到Block的类型有三种,也就是__block_impl中的isa指向的对象有三种类型:

截屏2022-06-12 15.50.27.png

那在ARC环境下,哪些情况下编译器会自动把栈区Block拷贝到堆上

截屏2022-06-12 15.53.09.png

当把Block拷贝到堆上,会有哪些变化

typedef void(^Block)(void);
int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc]init];
        Block block = ^{
            NSLog(@"%p",obj);
        };
        block();
    }
}

通过Clang指令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m 转成C++代码

截屏2022-06-12 16.04.34.png

可以看到捕获的对象被强引用了,进行了copy操作,在copy函数内部的_Block_object_assign会根据对象修饰符strong或者weak而对其进行强引用或者弱引用。

总结:

  • 当Block内部访问了对象类型的auto对象时,如果Block是在栈上,将不会对auto对象产生强引用。

  • 如果Block被拷贝到堆上,会调用Block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign会根据auto对象的修饰符(__strong,__weak,__unsafe_unretained)做出相应的操作,当使用的是__strong时,将会对auto对象的引用计数加1,当为__weak时,引用计数不变。

  • 如果Block从堆上移除,会调用block内部的dispose函数,内部会调用_Block_object_dispose函数,这个函数会自动释放引用的auto对象。

解决Block的循环引用

我们知道产生循环引用的条件是相互持有,就像下面这个图画的一样

截屏2022-06-12 16.19.28.png

要想解决这个问题,就是打破这个循环,通过__weak修饰对象

截屏2022-06-12 16.21.12.png

__unsafe_unretained也可以解决循环引用,不安全,指向的对象销毁时,指针存储的地址值不变

截屏2022-06-12 16.22.55.png

__block修饰对象,不用的时候把对象置为null,也一样可以打破循环

截屏2022-06-12 16.28.30.png

以上就是最近我整理的关于Block的知识点,文章中如有纰漏,希望大家指正。