iOS底层学习 - Block全解

1,975 阅读22分钟

对于Block的使用,相信大家都不会很陌生。但是对于其底层的原理可能了解的就不够透彻了,本章节来全面讲解一下Block相关知识

基本概述

概念

闭包 = 一个函数「或指向函数的指针」+ 该函数执行的外部的上下文变量「也就是自由变量」

BlockObjective-C 对于闭包的实现。

简单来说,block就是将一些代码封装起来,以便在将来某个时候被使用,如果你不去调用blockblock内部封装的代码就不会执行。

使用

block的使用格式如下:

1.普通使用

返回值类型(^block的名称)(参数类型)=^返回值类型(参数){...};

// 无参数无返回值
void(^MyBlockOne)(void) = ^(void){

NSLog(@"无参数,无返回值");  

};  
MyBlockOne();//block的调用
// 有参数无返回值
void(^MyblockTwo)(int a) = ^(int a){

NSLog(@"@ = %d我就是block,有参数,无返回值",a);

};  
MyblockTwo(100);
// 有参数有返回值
int(^MyBlockThree)(int,int) = ^(int a,int b){    

  NSLog(@"%d我就是block,有参数,有返回值",a + b);returna + b; 

};  
MyBlockThree(12,56);
// 无参数有返回值(很少用到)
int(^MyblockFour)(void) = ^{NSLog(@"无参数,有返回值");
    return45;
 };
MyblockFour();

2.当做属性使用

@property (nonatomic, copy) 返回值类型 (^block的名称)(参数类型);

//有返回值有参数的属性
@property (nonatomic, copy) int (^MyBlock)(NSString *name);
//没返回值没参数的属性
@property (nonatomic, copy) void (^MyBlock1)();

3、作为方法声明的参数

-(void)方法名:(返回值类型 (^)(参数类型))block的名称;

-(void)myBlock:(int(^)(NSString *name))completion;

4、实际开发中常用typedef 定义Block

typedef 返回值类型 (^类型名称)(参数类型);

typedef int (^MyBlock)(int , int);
这时,MyBlock就成为了一种Block类型
在定义类的属性时可以这样:
@property (nonatomic,copy) MyBlock myBlockOne;
在定义方法时可以这样:
-(void)myBlock:(MyBlock)completion;

Block的底层结构

首先我们通过clang命令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

将下面代码编译成C++代码,看其底层的实现

void(^block)(void) = ^{
    printf("MyBlock");
};
    
block();

转换后代码:

int main(){

    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

Block的定义

可以看到转化为的主要代码如下,在C++代码中,(void (*)()表示强制转换,去掉相关代码后,可以发现定义block时,主要就是调用了__main_block_impl_0构造函数

接下来查看__main_block_impl_0函数的定义,我们发现其内部其实就是进行了函数式的保存,转换为了__block_imp,最终会返回一个同名的结构体对象。

也就是说在定义block时,会调用__main_block_impl_0构造函数,并将生成的同名结构体指针赋值给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;
  }
};

接下来我们看一下传入的两个参数分别代表的是什么意思。

1. __main_block_func_0

寻找源码,我们可以发现__main_block_func_0就是封装了我们代码块中执行的相关逻辑,也就是说这是对代码块逻辑的一层封装。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    printf("MyBlock");
}

2. &__main_block_desc_0_DATA

通过源码,我们可以发现其调用__main_block_desc_0_DATA构造函数,返回了一个结构体,里面主要是包含了结构体__main_block_impl_0所占用的大小,最终又把他作为参数,传递给__main_block_impl_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)};

3. __block_impl

最终我们看一下生成的__block_impl结构体是什么样子的。通过源码我们发现:

  • 存在isa指针,和对象一样,表示block类型,下面会详细讲解
  • FuncPtr代表__main_block_func_0封装的代码块

所以__block_impl就主要包含了block的类型和代码块的封装

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

Block的执行

关于Block的执行,简化完的代码如下,即调用__block_impl中的FuncPtr,而FuncPtr就是封装的执行的代码块。

block->FuncPtr

总结一下,关于block的本质:

  1. block本质也是一个OC对象,其内部也有isa指针和大小
  2. block是封装了函数调用函数调用环境的OC对象

Block的变量捕获

在使用block时,我们一般都会使用一些外部的变量,那么block内部是如何处理这些变量的呢?

自动捕获

block的自动捕获有一个经典的小栗子,如下代码所示,可以很好的发现,block内部此时并不会改变age的值。

int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);
};
age = 18;
block();
---------------------------------------
打印结果:
10

我们转换代码后,发现再生成__main_block_impl_0结构体时,增加了一个age成员变量,并将外部的值赋值给它,做了相应的值拷贝的操作。且我们在调用代码块中的逻辑,进行打印时,生成了一个同名的临时变量,所以是不能够进行修改的。

局部静态变量捕获

int age = 10;
static int height = 10;
void (^block)(void) = ^(){
NSLog(@"Age is %d, height is %d", age, height);
};
//先修改age和height的值
age = 20;
height = 20;
//Block的调用
block();
--------------------------------------------------
打印结果:
Age is 10, height is 20

通过上述的打印结果,我们发现局部的静态变量的值,在block内部是被修改了的。那说明局部变量并不是简单的值拷贝,接下来我们继续看源码。

通过源码,我们可以发现,对于静态的局部变量,block内部其实是生成的是一个指针地址,在调用方法时,也是通过其指针地址,获取的值,所以外部的修改,在其内部也是生效的。

全局变量

对于全局的变量,我们通过源码可以发现,block内部并没有进行捕获,只需要在要用的时候,直接通过变量名访问就行了,因为全局变量时跨函数的,可以直接通过变量的名字直接访问。

__block关键字的捕获

int main(){
    __block int age = 10;
    static int height = 10;
    void(^block)(void) = ^{
        age++;
        printf("Age is %d, height is %d", age, height);
    };
    block();
    return 0;
}
--------------------------------------------------
打印结果:
Age is 11, height is 10

通过上面的例子,我们知道局部变量在block内部是值传递,是不能进行修改的。但是当我们在变量前加入__block关键字时,在其内部就可以对值进行修改,那么到底是怎么实现的呢,我们继续探究源码。

转换代码后,我们可以发现经过__block修饰的变量,会转变成__Block_byref_age_0类型的结构体对象,并带有5个变量。

  • __isa:结构体中存在的这个isa指针也就说明了__Block_byref_a_0本质是一个对象
  • __forwarding__forwarding__Block_byref_a_0结构体类型的,并且__forwarding存储的值为(__Block_byref_a_0 *)&a,即结构体自己的内存地址指针。
  • __flag:C++预留参数,为0
  • __size:sizeof(__Block_byref_a_0)__Block_byref_a_0所占用的内存空间。
  • age:用来存储原变量值,这里存储10。

在调用的时候,就比较简单了,先获取到__Block_byref_age_0对象,然后获取到__forwarding中指针地址,最后对其值进行修改。

总计一下,关于block的捕获机制

  1. auto局部变量是直接在栈上保存的,所以block如果要使用的话,必须值拷贝到其内部空间中,使用的时访问的是内部生成的临时变量
  2. static的变量在运行的生命周期中都存在,所以block使用时,会进行指针拷贝,获得其值进行修改
  3. 全部变量不会被捕获,在使用的时候直接根据变量名进行访问
  4. __block修饰的局部变量,回来内部生成__Block_byref_a_0结构体对象,里面保存着isa指针,自己的指针地址,内存大小和局部变量的值,并保存在block空间中,使用时会获取到对象的指针地址的值进行操作。

Block的内存管理

程序内存小概念

程序内存主要分为以下几种:

  • 代码段 占用空间很小,一般存放在内存的低地址空间,我们平时编写的所有代码,就是放在这个区域
  • 数据段 用来存放全局变量
  • 堆区 是动态分配内存的,用来存放我们代码中通过alloc生成的对象,动态分配内存的特点是需要程序员申请内存和管理内存。例如OC中alloc生成的对象需要调用releas方法释放【MRC下】,C中通过malloc生成的对象必须要通过free()去释放。
  • 栈区 系统自动分配和销毁内存,用于存放函数内生成的局部变量

Block类型

我们的Block在平时开发中,根据内存的存储,主要就是分为3种类型,但是其实根据libclosure-73的源码,我们发现还有另外3中系统使用的block类型,我们就不做研究了,主要是看日常开发用到的3种。

Block的常用的3中类型分别为:

  • __NSGlobalBlock__ ( _NSConcreteGlobalBlock ):全局Block
  • __NSStackBlock__ ( _NSConcreteStackBlock ):栈Block
  • __NSMallocBlock__ ( _NSConcreteMallocBlock ):堆Block

__NSGlobalBlock__

// 无变量
- (void)viewDidLoad {
    [super viewDidLoad];
    
    void (^block)(void) = ^{
       
    };
    
    block();
    NSLog(@"%@",[block class]);
}

// 静态变量
- (void)viewDidLoad {
    [super viewDidLoad];
    static int a = 10;
    void (^block)(void) = ^{
        NSLog(@"%d",a);
    };
    
    block();
    NSLog(@"%@",[block class]);
}

// 全局变量
static int a = 10;
- (void)viewDidLoad {
    [super viewDidLoad];
    
    void (^block)(void) = ^{
        NSLog(@"%d",a);
    };
    
    block();
    NSLog(@"%@",[block class]);
}
--------------------------------------------------------------
打印结果:
__NSGlobalBlock__

根据以上的例子我们可以知道,当Block没有变量的捕获或者是全局变量时,此时Block是在data段的,即全局的Block。一般情况下我们比较少用到。

__NSStackBlock__

当我们的block捕获外界变量局部变量时,此时Block的类型为__NSStackBlock__。下面我们写个例子:

- (void)viewDidLoad {
    [super viewDidLoad];
    int a = 10;
    void (^block)(void) = ^{
        NSLog(@"%d",a);
    };
    
    block();
    NSLog(@"%@",[block class]);
}
--------------------------------------------------------------
打印结果:
__NSMallocBlock__

根据上面的结论,我们发现貌似是不对的,此时的block是__NSMallocBlock__。那么为什么会产生这种结果呢,这是由于在ARC环境下,会将Block自动copy到堆区,那么为什么要进行这样的操作呢?

我们知道在栈上的变量是超过其作用域之后,会被系统自动释放,而不需要我们手动进行release,我们定义的局部block自然也是要销毁的,但是如果此时我们block被销毁了,此时调用就会有有问题。

那么为了解决上述的问题,在ARC下,系统就会自动将Block复制到堆上,从而延长其生命周期。Block的复制操作执行的是copy实例方法。Block只要调用了copy方法,栈块就会变成堆块。

在copy操作之后,既然__block变量也被copy到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,例如以下的情况

  • block作为函数参数返回的时候
  • 将block复制给__strong指针的时候
  • block作为Cocoa API中方法名里面含有usingBlock的方法参数时
  • block作为GCD API的方法参数的时候

__NSMallocBlock__

当我们直接调用临时变量时,此时block会在堆区。

NSLog(@"GlobalBlock11:%@",^{ NSLog(@"GlobalBlock--%d",a);});
--------------------------------------------------------------
打印结果:
__NSMallocBlock__

总结一下Block的内存管理:

使用Copy后

Block的循环引用

我们都知道,造成循环引用的原因是 self->block->self,这两者之间的相互强引用造成的。

Block对象的捕获

那么首先我们来看一下,Block为啥会对self或者对象进行强引用呢? 我们可以看一下对象在Block中是如何存在的。

#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        void (^block1)(void) = ^{
            NSLog(@"MyBlock - %d",person.age);
        };
        block1();
    }
    return 0;
}

使用上述代码,转换为C++代码可以清晰的看到Person对象被强引用,所以当Person销毁后,如果block不销毁,则Person不会进行释放。

copy和dispose

我们发现在捕获变量和__block类型等局部变量时,系统会自动生成__main_block_copy_0__main_block_dispose_0函数并调用

我们发现__main_block_copy_0内部调用了_Block_object_assign函数,并传入了person的地址,其内部的原理实现为对根据传入对象的类型,选择进行强引用还是弱引用,并在block进行copy操作时进行调用

  • 第一个dst代表拷贝之前栈空间的block,
  • 第二个src代表拷贝之后堆空间上的block。

同理__main_block_dispose_0也是在block进行销毁时进行对应的操作

循环引用处理

通过上面的探究,我们基本了解了,在block进行copy到堆上的操作时,会对传入的对象类型进行强引用或者弱引用,如果是强引用,那么必然会导致block在堆上强持有对象,造成对象的不能及时释放。所以我们需要对传入的对象进行弱引用,这样block也会弱引用,并不会增加引用计数,造成不释放。

通常在开发中使用__weak typeof(self) weakSelf = self;将其添加到弱引用表中,在block内部使用weakSelf来处理。但是如果block内部调用时,self已经释放,此时使用weakSelf处理就行不通了,这时候可以添加__strong typeof(weakSelf) strongSelf = weakSelf;来对弱引用进行一下强引用,这样等到出了block作用域之后,就都会销毁了,不造成循环引用。

    __weak typeof(self) weakSelf = self; // weakSelf(弱引用表) -> self
    // strongSelf(nil) -> weakSelf -> self(引用计数不处理)--nil -> block -> weakSelf
    self.block = ^{
        // 持有 不能是一个永久持有 - 临时的
        __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
    };
    self.block();

总结一下,Block的循环引用

  • Block内部会对对象类型进行持有
  • Block在进行copy操作时,会调用__main_block_copy_0进行处理,对捕获的对象,根据其修饰类型来进行强引用或者弱引用
  • Block在进行dispost操作时,会调用__main_block_dispose_0进行处理,也是根据修饰类型来进行对应的释放
  • 处理循环引用时,可以采用弱引用对象,block内部强持有弱引用的对象的方式来处理,保证block内部获得的对象为弱引用,不增加引用计数,这样就能避免循环引用

Block底层源码验证

我们通过编译后的C++代码,已经基本掌握了Block的相关知识,但是对于底层源码的处理逻辑还不是很清晰,比如block是如何copy到堆上的,我们可以通过libclosure源码进行查看。

通过查看C++的转换代码,可以发现其重要Block代码都在Block_private.h

在libclosure源码中,我们找到了Block_layout结构和我们在C++代码中看到的Block是基本相同的,所以这应该就是Block的基类

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor; //
    // imported variables
};

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

  • isa:指向父类的指针
  • flags:记录状态的标志位
  • reserved:
  • invoke:执行代码块函数
  • descriptor:block的附加描述信息

关于flags参数,是以位域的形式存在,主要用来记录block的一切标志信息

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime =(0x0001)释放标记,一般常用
    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
};
  • 第1位:释放标记,一般常用BLOCK_NEEDS_FREE做位与操作,一同传入Flags,告知该block可释放
  • 第16位:存储引用计数的值,是一个可选用参数
  • 第24位:第16位是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的值
  • 第25位:是否拥有拷贝辅助函数
  • 第26位:是否拥有block析构函数
  • 第27位:标志是否有垃圾回收
  • 第28位:标志是否是全局block
  • 第30位:与BLOCK_USE_STRET相对,判断是否当前block拥有一个签名。用于runtime时动态调用

我们发现除了Block_descriptor_1,还有Block_descriptor_2Block_descriptor_3等可选的参数,Block_descriptor_2中含有copy和dispose两个函数,Block_descriptor_3有signature和layout。

copy和dispose两个函数通过上面C++的分析,我们知道在捕获对象等局部变量时,会生成这两个函数

#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;  // Block签名
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

在源码runtime.cpp中,我们可以找到这两个方法的实现。通过方法的实现,我们发现两个方法都会先判断flags标志位是否含有对应的值,即BLOCK_HAS_COPY_DISPOSEBLOCK_HAS_SIGNATURE

而当满足标志位时,怎么获取到对应的对象呢,我们可以看到两个函数,都是通过Block_descriptor_1加上对应的大小,进行内存偏移得来的,说明这3个对象在内存分布上是连续的。

static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}

Block的Copy

通过上面的探究我们知道,当Block为栈Block时,会自动进行copy操作到堆Block,那么这个过程是如何进行的,我们接着使用源码验证

仍然使用上述的代码,打断点看汇编相关代码

通过汇编代码,我们知道Block调用了objc_retainBlock函数,然后再objc_retainBlock函数中,又调用了_Block_copy函数,通过获取此时block的类型,我们可以发现block在经过此步骤后,变成了堆block,说明其就是copy的主要方法

下面我们看_Block_copy的相关源码,主要的copy逻辑可以总结如下:

  • 全局Block在执行copy时:不作任何处理,直接返回
  • 堆Block在执行copy时:会增加引用计数,然后返回
  • 栈Block在执行copy时:
    • 先会申请一片相同大小的内存空间,
    • 然后将栈区的Block拷贝到堆区,
    • 设置标志位的引用计数,
    • 并执行_Block_call_copy_helper对其中的_Block_descriptor_2进行copy操作
    • 最后设置Block的isa指向为堆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;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        ✅// 如果需要对引用计数进行处理,那就直接处理,处理完就返回
        ✅// block的引用计数是不由runtime下层处理,需要自己处理
        ✅// 这个地方处理的是堆区block
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        ✅// 如果是全局block 直接返回
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        ✅// 栈区block 使用copy
        ✅// 先在堆区初始化一块内存空间
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        ✅// 将栈区的数据copy到堆区的空间
        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.
        ✅// 设置为isa为_NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

Block的签名

通过Block_layot的结构,我们知道函数的签名是在Block_descriptor_3中的,如果我们要找到Block_descriptor_3,需要通过内存便宜,找到Block_descriptor_1,然后再偏移找到Block_descriptor_3中的signature,查看其签名。

如上述代码,打印出的签名为v8@?0,其中@?就表示Block

__block修饰原理

通过上面对C++的探究,我们知道,在捕获__block的变量是,在其底部会生成__Block_byref结构体对象。

通过源码我们可以发现,其结构和Block有异曲同工之妙,都有可选的Block_byref_2Block_byref_3参数,而且在使用__block修饰的时候,也会相对应的copy和dispose函数,与Block_byref_2对应,说明其也会进行对应的copy等操作。

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};

struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    BlockByrefKeepFunction byref_keep;
    BlockByrefDestroyFunction byref_destroy;
};

struct Block_byref_3 {
    // requires BLOCK_BYREF_LAYOUT_EXTENDED
    const char *layout;
};

我们发现在生成的函数中,也会调用对应的_Block_object_assign_Block_object_dispose函数,我们还是以copy来探究_Block_object_assign函数是如何进行拷贝的

//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
// hold objects - 自动捕获到变量
// lgname
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/
            
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *dest = object;
        break;

      default:
        break;
    }
}

通过查看源码,我们发现内部针对传入对象的类型不同,做了不同的处理,针对不同的枚举,其含义如下:

enum {
    // see function implementation for a more complete description of these fields and combinations
    BLOCK_FIELD_IS_OBJECT   =  3,  // 截获的是对象__attribute__((NSObject)), block, ...
    BLOCK_FIELD_IS_BLOCK    =  7,  // 截获的是block变量
    BLOCK_FIELD_IS_BYREF    =  8,  // 截获的是__block修饰的对象
    BLOCK_FIELD_IS_WEAK     = 16,  // 截获的是__weak修饰的对象
    BLOCK_BYREF_CALLER      = 128, // 处理block_bref内部对象内存的时候会加一个额外的标记,配合上面的枚举一起使用
};

1.BLOCK_FIELD_IS_OBJECT

如果修饰的是对象类型的,此时会调用_Block_retain_object(object);函数,通过远么可以发现,并没有做任何的处理,因为在ARC环境下,是有runtime底层来处理的

static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;

static void _Block_retain_object_default(const void *ptr __unused) { }

2.BLOCK_FIELD_IS_BLOCK

如果修饰的是block,则直接调用_Block_copy方法

3.BLOCK_FIELD_IS_BYREF

如果是__block类型,会调用_Block_byref_copy方法,通过源码,我们可以知道,主要做了一下几部操作:

  1. 申请堆内存空间
  2. 给新申请的空间赋值
  3. copy的对象和源对象都指向堆内存的拷贝地址
  4. 理desc2和3的内存偏移取值
  5. 如果已经在堆,则直接增加引用计数
static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        ✅// 1.申请堆内存空间
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        ✅// 2. 给新申请的空间赋值
        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;
        
        ✅// 3.copy的对象和源对象都指向堆内存的拷贝地址
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        
        copy->size = src->size;

        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
            ✅// 4.处理desc2 内存偏移取值 
            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) {
            ✅// 处理desc2 内存偏移取值 
                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;
            }

            (*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后,还调用了(*src2->byref_keep)(copy, src);这个方法,那么这个方法到底是干啥的呢,通过C++中生成_Block_byref对象时传入的参数,如果有cop函数时,这两个方法是会赋值给Block_byref_2的,也就是对应的拷贝(__Block_byref_id_object_copy_131)和析构函数(__Block_byref_id_object_dispose_131

查看其源码,发现其调用了_Block_object_assign,并偏移了40个字节,那么为什么是40呢,看下面的源码结构,我们可以发现,40个字节后,正好是name属性,也就是说,对name属性进行了copy

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
struct __Block_byref_name_0 {
    void *__isa;                                        // 8
    __Block_byref_name_0 *__forwarding;                 // 8
    int __flags;                                        // 4
    int __size;                                         // 4
    void (*__Block_byref_id_object_copy)(void*, void*); // 8
    void (*__Block_byref_id_object_dispose)(void*);     // 8
    NSString *name;
};

总结一下,__block的修饰原理

  • block的拷贝,从栈内存到堆内存。
  • 对新生成的结构体的拷贝。__block修饰的变量会生成一个名为__Block_byref_XXX_0结构体,将原来的进行了封装,会调用_Block_object_assign根据其类型进行申请内存,拷贝到堆等操作
  • 并对原来的对象的内存的拷贝。

关于Block的释放,和copy的原理是基本一样的,只不过是反过来,有兴趣的同学可以自己研究一下。

总结

  • Block本质也是一个OC对象,封装了函数的调用和调用环境,内部也有isa指针
  • Block的变量捕获
    • 对于局部变量的捕获实在栈上保存的,ARC下回自动拷贝到堆中使用
    • 对于静态变量,会进行指针拷贝,对值进行修改
    • 对于全局变量,不会进行捕获,直接访问
  • Block根据存放位置不同,可以分为3种
    • __NSGlobalBlock__ ( _NSConcreteGlobalBlock ):全局Block,无临时变量访问
    • __NSStackBlock__ ( _NSConcreteStackBlock ):栈Block,访问临时变量
    • __NSMallocBlock__ ( _NSConcreteMallocBlock ):堆Block,栈block调用copy
  • Block循环引用问题原因及处理
    • 原因:由于Block会对对象进行捕获,并根据其类型进行对应的copy到堆的处理,如果是强引用,对象则需要再block释放后才可以释放,如果对象也持有了block,则发生了循环引用
    • 解决:当发生copy操作时,采用弱引用,则不会增加引用计数,同时在block内部强引用此时弱引用的对象,防止弱引用对象被释放而造成的数据问题,可以有效避免循环引用
  • Block的Copy操作也会根据Block的类型来进行处理
    • 全局Block在执行copy时:不作任何处理,直接返回
    • 堆Block在执行copy时:会增加引用计数,然后返回
    • 栈Block在执行copy时:
      • 先会申请一片相同大小的内存空间,
      • 然后将栈区的Block拷贝到堆区,
      • 设置标志位的引用计数,
      • 并执行_Block_call_copy_helper对其中的_Block_descriptor_2进行copy操作
      • 最后设置Blockisa指向为堆Block
  • __block修饰原理:3层拷贝
    • block的拷贝,从栈内存到堆内存。
    • 对新生成的结构体的拷贝。__block修饰的变量会生成一个名为__Block_byref_XXX_0结构体,将原来的进行了封装,会调用_Block_object_assign根据其类型进行申请内存,拷贝到堆等操作
    • 并对原来的对象的内存的拷贝。

参考

iOS Block 详解

iOS底层原理探索— block的本质

Block笔记