底层探索-Block详解

281 阅读15分钟

说到Block,相信大家并不陌生,平时开发用的太多了,本篇章就从基础到深入,全方位的来进行分析。

一:Block初探

1. 什么是Block?

Block就是一个代码块, Block是将函数及其执行上下文封装起来的对象,是一个匿名的函数对象, Block也有 isa。既然Block内部封装了函数,那么它同样也有参数和返回值,本身也可以被作为参数在方法和函数间传递。

2. 基本使用

  • 普通用法 无参数无返回值
// 无参数无返回值
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();
  • 作为属性
//有返回值有参数的属性 @property (nonatomic, copy) int (^MyBlock)(NSString *name); //没返回值没参数的属性 @property (nonatomic, copy) void (^MyBlock1)();
  • 作为方法参数
-(void)myBlock:(int(^)(NSString *name))completion;
  • typedef 定义Block
typedef int (^customBlock)(int , int);
这时,customBlock
在定义类的属性时可以这样:
@property (nonatomic,copy) customBlock firstBlock;
在定义方法时可以这样:
-(void)customBlock:(customBlock)completion;

二:Block内存管理

1. Block分类

Block一共有三种类型:

  • __NSGlobalBlock__ 全局Block
  • __NSMallocBlock__Block
  • __NSStackBlock__Block

image.png

MRC

  • NSGlobalBlock

    block内部没有引用外部变量的,Block类型都是NSGlobalBlock类型,存储于全局数据区,由系统管理其内存,retaincopyrelease操作都无效。如果访问了外部static或者全局变量也是这种类型。

  • NSStackBlock

    block内部引用外部变量,retainrelease操作无效,存储于栈区,变量作用域结束时,其被系统自动释放销毁。

  • NSMallocBlock

    如上例中的[blockB copy]操作后变量类型变为NSMallocBlock,支持retainrelease,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数,当引用计数为零的时候释放。

ARC

  • NSGlobalBlock

    此种情况和MRC一样,block内部没有引用外部变量,或者只使用静态变量全局变量Block类型都是NSGlobalBlock类型。

  • NSStackBlock

    访问了外部变量,但没有强引用指向这个block,如直接打印出来的block 比如这样:

  • NSMallocBlock

    ARC环境下只要访问了外部变量,而且有强引用指向该block(或者作为函数返回值)就会自动将__NSStackBlock类型copy到堆上。

2 copy对Block的影响

栈上的Block,如果其所属的变量作用域结束,该Block就被废弃,如果一般的自动变量,当然,Block中的_block变量也被放弃。

image.png

为了解决这个问题,需要把Block复制到堆中,延长其生命周期。在ARC下,系统就会自动将Block复制到(copy)堆上。

image.png

不同类型的Block使用copy效果如下:

image.png

对于copy对不同Block的这一特性,我们来看一段代码。

    NSObject *obj = [[NSObject alloc]init];
    NSLog(@"==== %ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));
    
    void(^firstBlock)(void) = ^{
        NSLog(@"==== %ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));
    };
    firstBlock();

    void(^__weak secondBlock)(void) = ^{
        NSLog(@"==== %ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));
    };
    secondBlock();
    
    void(^thirdBlock)(void) = [secondBlock copy];
    thirdBlock();

image.png

结果是否出乎意料呢?

第一个打印1,肯定是没有疑问的,我们着重看下后面的345

  • firstBlock是一个堆区的block,这里捕获了obj这个外部变量会进行加一,而且栈区的obj拷贝到堆区又加一,所以打印结果为 3

  • 打印 4是因为这里secondBlock是栈block,没有进行拷贝,只是捕获,进行加一,所以为4

  • 最后打印5是因为[secondBlock copy]进行了拷贝操作,加一,再赋值给thirdBlock,所以打印结果为 5

三:Block循环引用

1. 循环引用的原因

循环引用本质在于两者相互持有,比如控制器self,有一个Block属性,Block调用中又引用self,形成self->block->self的闭环,两者都无法释放。

   self.jwBlock = ^{
       [self doSomething];
   };

正常释放

image.png

循环引用

image.png

2. 循环引用的解决

方式一:利用weak自动释放 __weak typeof(self) weakSelf = self;

   __weak typeof(self) weakSelf = self;
   self.jwBlock = ^{
       [weakSelf doSomething];
   };

weakSelf的存在打破了循环引用,两者都可正常释放,平时可能会看到这样的代码

    __weak typeof(self) weakSelf = self; 
    self.block = ^{
        //临时变量
        __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
    };
    self.block();

__strong typeof(weakSelf) strongSelf = weakSelf定义了一个临时强指针指向weakSelf,防止其提前释放。

方式二:手动释放

__block ViewController *vc = self;
self.name = @"jw";
self.block = ^{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", vc.name);
        vc = nil;
    });
};
self.block();

方式三:不使用self,传参

self.name = @"jw";
self.block = ^(ViewController *vc){
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", vc.name);
    });
};
self.block(self);

3. 其他

其实平时我们同样会使用到系统的一些Block,比如UIView动画[UIView animateWithDuration: animations:],是否会存在循环引用呢?当然我们需要区分对待。

[UIView animateWithDuration: animations:]是类方法,不被self持有(即self持有了view,但view没有实例化)所以不会循环引用。

Masonry中是否存在循环引用?

Monsary使用的block是当做参数传递的,即便block内部持有self,设置布局的view持有block,但是block不持有view,当block执行完后就释放了,self的引用计数-1,所以block也不会持有self,所以不会导致循环引用。

  • 使用Facebook的开源框架能检测是否存在循环引用
- (void)checkLeaks {
    FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
    [detector addCandidate:self];
    NSSet *retainCycles = [detector findRetainCycles];
    NSLog(@"%@", retainCycles);
}

四:Block底层结构

1. 通过clang看Block

1. Block 本质

定义.c的文件,添加如下代码

int main(){
    void(^block)(void) = ^{
        printf("我来了");
    };
    return 0;
}

使用clang命令clang -rewrite-objc main.c -o main.cpp将其转化为.cpp文件。

image.png

.cpp,当我们去掉一些类型强转代码,其实就是调用了__main_block_impl_0构造函数,其中传入了两个参数,分是__main_block_func_0、 &__main_block_desc_0_DATA

先看下__main_block_impl_0

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;
  }
};

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

__main_block_func_0 Block 内部实现

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("我来了");
}
  • __main_block_impl_0是一个结构体,继承于__block_impl,可以看到__block_impl里面是有isa的,这也说明Block也是对象。
  • __main_block_impl_0结构体内部有一个构造函数__main_block_impl_0,这个构造函数对block结构体中相关属性进行设置。
  • void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)中的第一个参数__main_block_func_0(上面贴了代码),其实就是具体的Block实现,传入到__main_block_impl_0,通过函数式(函数作为参数)的手法将其保存在impl.FuncPtr当中,方便随时调用。
  • Block调用的时候,实际上调用的是block->FuncPtr,并将block结构体作为参数传入到方法实现中(之所以传入Block作为参数,在__main_block_func_0中有一个参数__cself,在Block内部就持有了Block所有的东西,相当于一个隐藏参数,在内部提供了更多的便利性)。

2. Block 捕获变量

1. 自动捕获变量

还是使用上面的示例代码,添加int a = 10,然后转化为.cpp文件

image.png

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    printf("%d == 我来了",a);
}
  • __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a)多了一个参数a,而在__main_block_impl_0中同样自动生成了一个int a变量,这是在编译时就完成了。
  • __main_block_impl_0构造函数中,传入了_a,通过C++的语法a(_a)进行了赋值。
  • __main_block_func_0实现中,通过__cself->取到a的值并赋值给新定义的a,这两个a并不是同一个a,地址不同,这是值拷贝。

2. __ block修改外部变量

block中访问的外部变量是写操作不对原变量生效的,使用__block为什么就可以呢?

image.png image.png

我们发现多了一个__Block_byref_a_0结构体,

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_a_0 *a = __cself->a; // bound by ref
    (a->__forwarding->a)++;
    printf("%d == 我来了",(a->__forwarding->a));
}
  • 新定义的__Block_byref_a_0类型的a进行结构体赋值,其中(__Block_byref_a_0 *)&a,是将int a = 10a的地址复制给结构体中的第二个参数__forwarding
  • __main_block_impl_0中传入的第三个参数&a就是新定义的这个__Block_byref_a_0类型的a的地址。
  • __main_block_impl_0结构体中多了__Block_byref_a_0 *a,而*a就是第三个参数传入的&a
  • 而在构造函数中a(_a->__forwarding)进行赋值,在__main_block_func_0内部调用__Block_byref_a_0 *a = __cself->a__cself->a取到的a__cself->a地址相同,是指针拷贝,当调用(a->__forwarding->a)++,相当于外界a进行++。

image.png

2. 通过源码看Block

1. Block的copy操作

Block源码是开源的,可以再官网 libclosure 进行下载查看。

在上文通过clang查看代码的时候其实Block存在于Block_private.h

image.png

我们搜索Block_layout

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;
};

// 可选
#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

};

这与我们在C++中看到的代码基本相同

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
  • isa:指向父类的指针
  • flags:记录状态的标志位
  • reserved:保留字段
  • invoke:执行代码块函数
  • descriptorblock的附加描述信息

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

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

descriptor 会存在三种形式,BLOCK_DESCRIPTOR_1BLOCK_DESCRIPTOR_2BLOCK_DESCRIPTOR_3,后两者属于可选,内部包含有不同的函数。

#if 0

static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)
{
    return aBlock->descriptor;
}
#endif

static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    🌹// 判断是否有 BLOCK_HAS_COPY_DISPOSE 标识
    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)
{
    🌹// 判断是否有 BLOCK_HAS_SIGNATURE 标识
    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_HAS_COPY_DISPOSE,那么就会有BLOCK_DESCRIPTOR_2
  • 有标识码BLOCK_HAS_SIGNATURE,就会有BLOCK_DESCRIPTOR_3
  • Block_descriptor_2Block_descriptor_3是通过内存平移来获取。

Block访问自动变量,会自动将Block从栈区拷贝。

下面我们看一段代码

image.png

我们通过汇编来看下流程

image.png image.png

在上图断点位置,发现其中会调用objc_retainBlock,我们单步走来到objc_retainBlock,在此处读取x0,此时Block,的确如我们所想,是__NSStackBlock__

过掉objc_retainBlock,我们再读取x0,此时Block已经变成了__NSMallocBlock__

image.png

那么我们设想,在objc_retainBlock做了一些操作,通过汇编进入objc_retainBlock内部看下,调用了_Block_copy

image.png

_Block_copy源码

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在执行copy时:不作任何处理,直接返回
  • 堆Block在执行copy时:会增加引用计数,然后返回
  • 栈Block在执行copy时:
    • 先会申请一片相同大小的内存空间,
    • 然后将栈区的Block拷贝到堆区,
    • 设置标志位的引用计数,
    • 并执行_Block_call_copy_helper对其中的_Block_descriptor_2进行copy操作
    • 最后设置Block的isa指向为堆Block

2. Block三层拷贝

__block修饰的变量确实能在Block内部进行修改,原因在于会将这个变量重新封装成结构体,进行相应的指针拷贝,那内部是如何操作外部变量的呢?

在上面clang生产的.cpp文件中

image.png

这里的__main_block_copy_0__main_block_dispose_0其实就是对应Block_descriptor_2中的copydispose,底层分别调用了_Block_object_assign_Block_object_dispose

_Block_object_assign

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];
        ********/

        // objc 指针地址 weakSelf (self)
            // arc
        _Block_retain_object(object);
            // 持有
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:🌹 // block类型
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
            // block 被一个 block 捕获

        *dest = _Block_copy(object);
        break;

      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:🌹

      case BLOCK_FIELD_IS_BYREF:🌹 // __block类型

        /*******
         // 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;
    }
}

_Block_object_assign会对类型进行判断,然后执行相应的流程

  • BLOCK_FIELD_IS_OBJECT 对象类型 执行_Block_retain_object
  • BLOCK_FIELD_IS_BLOCK Block类型 执行_Block_copy
  • BLOCK_FIELD_IS_BYREF 使用__block修饰的类型 执行_Block_byref_copy
BLOCK_FIELD_IS_OBJECT

_Block_retain_object

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

_Block_retain_object方法无任何实现,在ARC环境下,是有runtime底层来处理的。但值得注意的多一点,会对传入的objc进行指针拷贝,从而使该Block持有该对象。

image.png

BLOCK_FIELD_IS_BLOCK

如果修饰的是block,则直接调用_Block_copy方法,拷贝到堆区。

BLOCK_FIELD_IS_BYREF

如果是__block类型,会调用_Block_byref_copy方法。

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;
        🌹 // 有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
            🌹// 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;
}
  1. 保存外界传入的结构体Block_byref
  2. 申请堆内存空间
  3. 给新申请的空间赋值
  4. copy的对象和源对象都指向堆内存的拷贝地址
  5. 利用desc2和3的内存偏移取值
  6. 如果已经在堆,则直接增加引用计数。

如果有拷贝能力if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE),会进行进一步操作,最后执行(*src2->byref_keep)(copy, src);

我们先看下面这段代码

image.png

转化为C++之后,我们发现在__Block_byref结构体中多了__Block_byref_id_object_copy__Block_byref_id_object_dispose

image.png

而下层就会识别并调用byref_keep函数,而它就是Block_byref_2结构体的第一个元素,而它与__Block_byref_id_object_copy函数等价。

struct Block_byref_2 {

    // requires BLOCK_BYREF_HAS_COPY_DISPOSE

    BlockByrefKeepFunction byref_keep; // 结构体 __block  对象

    BlockByrefDestroyFunction byref_destroy;

};

来到C++文件当中

image.png image.png

我们发现在这里又调用了_Block_object_assign,代码在上面已经贴出,此时是对__block修饰的对象进行拷贝,为什么说拷贝的是修饰的对象呢?看下代码

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_lg_name_0 {
  void *__isa;  // 8
__Block_byref_lg_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*);  // 5*8 = 40
 NSString *lg_name;
};

_Block_object_assign函数第一个参数传入(char*)dst + 40,这个dst就是__Block_byref_lg_name_0这个结构体,这里采用内存偏移40的方式正好取到lg_name,因此这里是多修饰的对象属性进行拷贝。

综上所述,对于__blcok修饰的对象,整个过程出现了三次拷贝:

  • 第一层 Block自身的拷贝 调用block_copy,从栈内存到堆内存。
  • 第二层 __Block_byref结构体的拷贝 __block修饰的变量会生成一个名为__Block_byref结构体,调用_Block_object_assign对其进行拷贝。
  • 第三层 __block修饰的如果是对象,调用_Block_object_assign进行第三次拷贝,只不过此时在此方法中会走到case BLOCK_FIELD_IS_OBJECT:{}这个节点。
_Block_object_dispose
void _Block_object_dispose(const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        // get rid of the __block data structure held in a Block
        _Block_byref_release(object);
        break;
      case BLOCK_FIELD_IS_BLOCK:
        _Block_release(object);
        break;
      case BLOCK_FIELD_IS_OBJECT:
        _Block_release_object(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        break;
      default:
        break;
    }
}

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->forwarding;
    
    if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
        int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
        os_assert(refcount);
        if (latching_decr_int_should_deallocate(&byref->flags)) {
            if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
                struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                (*byref2->byref_destroy)(byref);
            }
            free(byref);
        }
    }
}

  • 如果是释放对象就什么也不做(自动释放)。
  • 如果是__block修饰,就将指向指回原来的区域并使用free释放。

五:总结

  • 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_copy,从栈内存到堆内存。
    • 第二层 __Block_byref结构体的拷贝 __block修饰的变量会生成一个名为__Block_byref结构体,调用_Block_object_assign对其进行拷贝。
    • 第三层 __block修饰的如果是对象,调用_Block_object_assign进行第三次拷贝,只不过此时在此方法中会走到case BLOCK_FIELD_IS_OBJECT:{}这个节点。

参考

iOS探索 全方位解读Block
一道Block面试题的深入挖掘
深入理解iOS的block