block 底层原理

一、block 简介

1.1 block 分类

  • NSGlobalBlock
    • 位于全局区。
    • Block内部不使用外部变量,或者只使用静态变量和全局变量。
  • NSMallocBlock
    • 位于堆区。
    • Block内部使用局部变量或者OC属性,并且赋值给强引用或者copy修饰的变量。
  • NSStackBlock
    • 位于栈区。
    • NSMallocBlock一样,可以在内部使用局部变量或者OC属性。但是不能赋值给强引用或者copy修饰的变量。

验证:

void (^globalBlock)(void) = ^{
    NSLog(@"globalBlock");
};

int a = 10;
void (^mallocBlock)(void) = ^{
    NSLog(@"mallocBlock - %d",a);
};

void (^__weak stackBlock)(void) = ^{
    NSLog(@"stackBlock - %d",a);
};

NSLog(@"globalBlock: %@\nmallocBlock: %@\nstackBlock: %@",globalBlock, mallocBlock, stackBlock);

输出:

globalBlock: <__NSGlobalBlock__: 0x10982a050>
mallocBlock: <__NSMallocBlock__: 0x6000011c1950>
stackBlock: <__NSStackBlock__: 0x7ffee63d60c0>

ARC下需要使用__weak修饰才是stackBlockblock持有的是^{}的指针,指向它的内存空间。

1.2 block 拷⻉到堆 block

⚠️ 前提是捕获了局部变量或者OC属性。

  • 手动copy
NSObject *objc = [NSObject alloc];
void(^ __weak weakBlock)(void) = ^{
    NSLog(@"%@",objc);
};
//copy
NSLog(@"weakBlock: %@,weakBlock copy: %@",weakBlock,[weakBlock copy]);
//weakBlock: <__NSStackBlock__: 0x7ffee3a8e5c8>,weakBlock copy: <__NSMallocBlock__: 0x60000316d860>
  • block作为返回值。
- (void(^)(void))returnBlock {
     int a = 0;
    return ^{
        NSLog(@"%d",a);
    };
}
//调用
NSLog(@"return block: %@",[self returnBlock]);

//输出
return block: <__NSMallocBlock__: 0x600002ad0f60>

目前在ARC下在运行时默认会自动copy成堆block,在MRC下返回栈block会直接报错,需要copy到堆区再返回:

image.png

  • 被强引用或者copy修饰(捕获了局部变量/oc属性)。默认情况下就是__strong
void(^strongBlock)(void) = ^{
    NSLog(@"%@",objc);
};
  • 系统API包含usingBlock
- (void)block_copy_usingBlock {
    NSArray *arrar = @[@1,@2,@3];
    void (^usingBlock)(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) = ^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
    };
    [arrar enumerateObjectsUsingBlock:usingBlock];
    NSLog(@"%@",usingBlock);
}

这个时候定义的usingBlock是全局block,输出也是。不过打断点后底层调用了_Block_copy(最终不会真的copy,全局block就直接返回了):

image.png

当自己定义的函数有block参数时:

int a = 10;
[self block:^{
    NSLog(@"%d",a);
}];
NSLog(@"end");

- (void)block:(void (^)(void))block {
    block();
    NSLog(@"%@",block);
}

block本身是栈block,不会调用_Block_copy。当内部使用NSLog打印block时打印的是堆block,这个时候调用了_Block_copy

image.png

NSLog打印时block转成OC对象进行了Copy

1.3 block 案例分析

1.3.1 block 捕获变量的引用计数

NSObject *objc = [NSObject new];
//1
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));

//1 + 2 (栈1 + 堆 1)
void(^strongBlock)(void) = ^{
    NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
strongBlock();
//栈1
void(^__weak weakBlock)(void) = ^{
    NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
weakBlock();
//堆1
void(^mallocBlock)(void) = [weakBlock copy];
mallocBlock();

输出:

retain count: 1
retain count: 3
retain count: 4
retain count: 5
  • objc创建后引用计数1。输出1。后续的block都对objc进行了持有。
  • strongBlock是一个堆block,从栈拷贝到堆(栈+1,堆+1)引用计数+2。输出3
  • weakBlock是一个栈block,引用计数+1。输出4
  • mallocBlock是从weakBlock拷贝的,是一个堆block引用计数+1。输出5
  • 这里weakBlockmallocBlock是对strongBlock的拆分。

当将objc使用__weak修饰时:

NSObject *objc = [NSObject new];
//1
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));

__weak NSObject *weakObjc = objc;
//放入弱引用表中,+1 = 2
NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(weakObjc)));

void(^strongBlock)(void) = ^{
    //弱引用表中已经存在,仍然是2
    NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(weakObjc)));
};
strongBlock();

void(^__weak weakBlock)(void) = ^{
    //弱引用表中已经存在,仍然是2
    NSLog(@"retain count: %ld",CFGetRetainCount((__bridge CFTypeRef)(weakObjc)));
};
weakBlock();

void(^mallocBlock)(void) = [weakBlock copy];
mallocBlock();

输出:

retain count: 1
retain count: 2
retain count: 2
retain count: 2
retain count: 2

weakObjc是一个__weak修饰的变量被放入了弱引用表,本身不会对引用计数产生变化,在调用CFGetRetainCount的时候内部会调用objc_loadWeakRetained对原始对象引用计数+1,然后调用retainCount,最后调用objc_release对引用计数-1,所以输出的引用计数会多1

1.3.2 block 的内存拷贝

int a = 0;
void(^ __weak weakBlock)(void) = ^{
    NSLog(@"%d", a);
};
struct _HPBlock *blc = (__bridge struct _HPBlock *)weakBlock;
//stack  block,只是赋值
id __strong strongBlock = weakBlock;
blc->invoke = nil;
//类型强转
void(^strongBlock1)(void) = strongBlock;
strongBlock1();

_HPBlock是模仿block的底层结构写了一个结构体,block底层就是结构体。主要是为了修改invokeinvoke在底层是一个函数指针指向代码块地址。这样其实能达到hook block的目的。

上面的代码运行会报错:

image.png

因为将blc指向了weakBlock设置invokenil从而导致callout的时候找不到invoke
修改strongBlock如下:

//id __strong strongBlock = weakBlock;
//栈 -> 堆
id __strong strongBlock = [weakBlock copy];

这个时候运行就没有问题了,strongBlock是对weakBlock的一份copy。在拷贝之后对blcinvoke置为nil相当于是对栈中blockinvoke进行的修改,对堆中的strongBlock没有影响。
如果block本身已经在堆中了再对其copy只是引用计数+1,这个时候修改invoke也会影响到堆中copyblock

  • block从栈中copy到堆中是深拷贝,堆中block拷贝是引用计数增加。

1.3.3 block 堆栈释放差异

- (void)testBlock3 {
    int a = 10;
    void(^__weak weakBlock)(void) = nil;
    {
        //代码块中 strongBlock 捕获 a
        void(^__weak strongBlock)(void) = ^{
            NSLog(@"a:%d", a);
        };
        //栈block给到weakBlock,weakBlock是栈block
        weakBlock = strongBlock;
    }
    weakBlock();
}

上面的代码运行输出:

a:10

strongBlock是一个栈block捕获了a,然后将strongBlock赋值给weakBlock此时weakBlock也是栈block。出了代码块作用域后调用栈block仍然有用。因为栈block的作用域是函数的栈中由系统自动回收,与代码块无关。
在代码块中代码执行完毕后此时栈中数据为:self - testBlock3 - a - weakBlock。可以通过读取栈中数据进行验证,修改代码如下:

- (void)testBlock3 {
    int a = 10;
    void(^__weak weakBlock)(void) = nil;
    {
        //代码块中 strongBlock 捕获 a
        void(^__weak strongBlock)(void) = ^{
            NSLog(@"a:%d", a);
        };
        weakBlock = strongBlock;
        NSLog(@"weakBlock:%@ strongBlock:%@",weakBlock,strongBlock);
    }

    //验证栈中数据
    void *sp  = (void *)&self;
    void *end = (void *)&weakBlock;
    long count = (sp - end) / 0x8;

    for (long i = 0; i <= count; i++) {
        void *address = sp - 0x8 * i;
        if (i == 1) {//SEL
            NSLog(@"%p : %s",address, *(void **)address);
        } else if (i == 2) {//int
            //由于int存储在高32位中,所以右移还原
            NSLog(@"%p : %d",address, *(uint64_t*)(address) >> 32);
        } else {//object
            NSLog(@"%p : %@",address, *(void **)address);
        }
    }
    //end

    weakBlock();
}

int数据只占用4字节,所以右移32位还原。输出:

0x7ffee8852118 : 
0x7ffee8852110 : testBlock3
0x7ffee8852108 : 10
0x7ffee8852100 : <__NSStackBlock__: 0x7ffee88520d0>

由于栈中内存分配从高到低,所以这里输出顺序与上面分析完全相反。当然也可以直接lldb调试查看:

image.png

strongBlock__weak修饰去掉:

- (void)testBlock3 {
    int a = 10;
    void(^__weak weakBlock)(void) = nil;
    {
        //代码块中 strongBlock 捕获 a
        void(^strongBlock)(void) = ^{
            NSLog(@"a:%d", a);
        };
        //堆block给到 weakBlock 两个都变为堆block
        weakBlock = strongBlock;
        NSLog(@"weakBlock:%@ strongBlock:%@",weakBlock,strongBlock);
    }
    weakBlock();
}

输出:

weakBlock:<__NSMallocBlock__: 0x600000309050> strongBlock:<__NSMallocBlock__: 0x600000309050>

这个时候运行输出block类型后就崩溃了,因为strongBlock是堆block作用域是代码块中(出了作用域会调用_Block_release)。而weakBlock是弱引用(也是堆block,与strongBlock指向同一块内存空间)指向strongBlock没有强持有strongBlock出了作用域后strongBlock就释放了,weakBlock就为nil了。

image.png

此时修改aNSObject类型的变量objc

- (void)testBlock3 {
    NSObject *objc = [NSObject alloc];
    void(^__weak weakBlock)(void) = nil;
    {
        //代码块中 strongBlock 捕获 objc
        void(^__weak strongBlock)(void) = ^{
            NSLog(@"objc:%@", objc);
        };
        weakBlock = strongBlock;
        NSLog(@"weakBlock:%@ strongBlock:%@",weakBlock,strongBlock);
    }

    //验证栈中数据
    void *sp  = (void *)&self;
    void *end = (void *)&weakBlock;
    long count = (sp - end) / 0x8;

    for (long i = 0; i <= count; i++) {
        void *address = sp - 0x8 * i;
        if (i == 1) {//SEL
            NSLog(@"%p : %s",address, *(void **)address);
        }
        else {//object
            NSLog(@"%p : %@",address, *(void **)address);
        }
    }
    //end
    weakBlock();
}

输出:

weakBlock:<__NSStackBlock__: 0x7ffee5b1d0d0> strongBlock:<__NSStackBlock__: 0x7ffee5b1d0d0>
0x7ffee5b1d118 : 
0x7ffee5b1d110 : testBlock3
0x7ffee5b1d108 : 
0x7ffee5b1d100 : <__NSStackBlock__: 0x7ffee5b1d0d0>
objc:(null)

可以看到此时blockobjc输出null,而objc在栈中是存在的。a是值拷贝,objc是引用计数增加。
如果weakBlockstrongBlock都是强引用那么此时blockobjc是可以输出的。那么说明栈block在捕获的对象在出了作用域后释放了。也就是说如果block是一个assign或者weak修饰的属性,那么捕获的对象也是被释放的。

  • 栈的作用域在函数栈区,堆的作用域在代码块中。栈block作用域同理。
  • block捕获对象是指针引用,捕获基本数据类型是值拷贝。栈block捕获的对象类型在出了作用域后对象就被释放了。
  • weak或者assign修饰的block在出了函数栈后block被释放,为空;在出了作用域后捕获的对象被释放,为空。

1.3.4 栈block

- (void)blockHeap {
    int a = 0;
    void(^ __weak block)(void) = ^{
        NSLog(@"---%d", a);
    };
    dispatch_block_t dispatch_block = ^{
        block();
    };
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
}

以上代码在执行后会crash,由于延迟调用的原因出了函数后栈block被释放了。
这个时候可以加个阻塞就不会崩溃了:

- (void)blockHeap {
    int a = 0;
    void(^ __weak block)(void) = ^{
        NSLog(@"---%d", a);
    };
    dispatch_block_t dispatch_block = ^{
        block();
    };
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
//    sleep(3);
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}

那么如果block是一个堆block呢?

- (void)blockHeap {    
    int a = 0;
    void(^block)(void) = ^{
        NSLog(@"---%d", a);
    };
    dispatch_block_t dispatch_block = ^{
        block();
    };
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
}

运行不会有问题。两个的区别是__weak__strongdispatch_block对于这两个的捕获是与修饰符对应的。这也就是block遇弱捕弱,遇强补强

二、block 的循环引用

正常对象的释放流程如下:


image.png

block和对象互相持有时:

image.png

2.1 案例1

typedef void(^HPBlock)(void);

@property (nonatomic, copy) HPBlock block;
@property (nonatomic, copy) NSString *name;

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

以上案例会造成循环引用self -> block -> self。这个时候使用__weak修饰self就可以解决循环引用了:

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

这个时候持有链变成了self -> block -> weakSelf(nil) -> block所以不会造成循环引用。在block内部执行一个延时操作:

self.name = @"HotpotCat";
__weak typeof(self) weakSelf = self;
self.block = ^(){
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",weakSelf.name);
    });
};
self.block();

在快速退出页面后,打印的时候self已经释放了。这个时候需要在block内部对weakSelf进行强引用:

self.name = @"HotpotCat";
__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();

这个时候持有链变成了self -> block -> strongSelf(临时变量) —> weakSelf(nil) > block。这个时候strongSelf只是一个临时变量,出了作用域空间strongSelf就释放了。当延迟函数没有执行完毕退出页面的时候并不马上释放,等到执行完毕后才进行释放。weakSelf会自动置空,在block内部strongSelfweakSelf进行了短暂的持有。这种解决循环引用的方式是 weak-strong-dance(强弱共舞)

那么修改为网络请求呢?

__weak typeof(self) weakSelf = self;
[[[NSURLSession sharedSession] dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.raywenderlich.com"]] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",strongSelf);
    });
}] resume];

在进入页面后立马退出,输出:

(null)

与上面的区别是在执行完NSURLSession的时候weakSelf已经是nil了,内部strongSelf指向的就是nil。(这里没有互相持有)。

__weak 是将修饰的变量放入弱引用表,用的时候从弱引用表取出。取的时候如果为nil了就赋值nil,否则就赋值它所指向的对象给临时变量,临时变量出了作用域后自动释放。这也就是weak能解决循环引用的根本原因。

2.2 五种解决循环引用的方式

除了上面说大家比较熟悉的 weak-strong-dance(强弱共舞) 解决循环引用的方式,还有4种方式。

2.2.1 临时变量置nil

__block ViewController *vc = self;
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然后用__block修饰,这个时候的持有关系是vc -> self -> block -> vc,但是在使用完毕后vc = nil切断了循环引用的链条也就断开了循环引用。

⚠️ 当然也可以在调用后或者在block内部调用完成时直接设置self.block = nil。一般这么做没什么意义,block设置为属性的目的就是为了多次使用,如果只使用一次就释放直接局部block就可以了。

2.2.2 self 作为 block 参数

typedef void(^HPBlock)(ViewController *);

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

self作为了参数传递给blockblock内部是对参数的引用,不会造成循环引用。

⚠️另外两种待补充

2.3 案例2

static ViewController *staticSelf;

- (void)weak_static {
    __weak typeof(self) weakSelf = self;
    staticSelf = weakSelf;
}

以上案例在页面退出的时候dealloc不会执行造成了内存泄露。此时持有关系是staticSelf -> weakSelf -> self
那么weakSelf为什么没有释放呢?
本质上staticSelf 、weakSelf 、self都指向同一块内存空间:

image.png

staticSelf是全局变量并不会释放,所以即使退出页面页面也不会释放。

2.4 案例3

typedef void(^HPBlock)(void);

@property (nonatomic, copy) HPBlock block1;
@property (nonatomic, copy) HPBlock block2;

- (void)block_weak_strong {
    __weak typeof(self) weakSelf = self;
    self.block1 = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        weakSelf.block2 = ^{
            NSLog(@"%@",strongSelf);
        };
       weakSelf.block2();
    };
   self.block1();
}

以上代码会造成循环引用。虽然strongSelf是一个临时变量,但是strongSelfblock2持有了(出了block1代码块仍然不会释放),block2又被self持有就有了self -> block2 -> strongSelf(与self指向同一块内存)。这个时候由于strongSelf不会自动置为nil所以会造成循环引用。

即使不调用block2也会造成循环引用,因为赋值的时候已经持有了。

那么如果修改如下呢?

- (void)block_weak_strong {
    self.block = ^{
        void (^block1)() = ^ {
            NSLog(@"%@",self);
        };
        block1();
    };
   self.block();
}

这个时候仍然会造成循环引用,由于 block的捕获是逐层捕获的

image.png

2.4.1 强弱共舞解决循环引用

这个时候如果block2中直接使用weakSelf或者再对strongSelf转一次weak就可以解决问题了:

__weak typeof(self) weakSelf = self;
self.block1 = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    __weak typeof(strongSelf) weakSelfAgain = strongSelf;
    weakSelf.block2 = ^{
        //NSLog(@"%@",weakSelf);
        NSLog(@"%@",weakSelfAgain);
    };
   weakSelf.block2();
};
self.block1();

此时如果block2中还有延迟函数就又要进行一次临时变量强持有了:


__weak typeof(self) weakSelf = self;
self.block1 = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    __weak typeof(strongSelf) weakSelfAgain = strongSelf;
    weakSelf.block2 = ^{
        __strong typeof(weakSelfAgain) strongSelfAgain = weakSelfAgain;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelfAgain.name);
        });
    };
   weakSelf.block2();
};
self.block1();

就无限套娃就完事了。

2.4.2 临时变量置为nil

__weak typeof(self) weakSelf = self;
self.block1 = ^{
    __block ViewController* vc = weakSelf;
    weakSelf.block2 = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
            vc = nil;
        });
    };
   weakSelf.block2();
};
self.block1();

对于block2中有延时任务的时候也可以通过临时变量使用完置为nil解决循环引用,当然没有延时任务直接使用weakSelf就完事了。

前面分析了block的分类以及循环引用,那么有以下疑问:

  1. block copy后就从栈区到堆区了,究竟是怎么处理的呢?
  2. block是如何捕获变量的呢?
  3. block捕获的对象是怎么保存和释放的呢?

三、block 的 Clang 底层分析

一个最简单的block:

#include 

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

通过clang编译成.cpp文件,此时main函数变成了:

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;
    //(__main_block_impl_0)函数调用的结果取地址给到 block,参数是 __main_block_func_0,&__main_block_desc_0_DATA 以及 a
    void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
    //调用 block->FuncPtr,参数是 block 自己
    block->FuncPtr(block);
    return 0;
}
  • void(^block)(void)相当于__main_block_impl_0的调用结果赋值给它,参数是__main_block_func_0&__main_block_desc_0_DATA以及a
  • block()执行相当于block调用FuncPtr参数是自己。

3.1 __main_block_impl_0

__main_block_impl_0是一个结构体:

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

通过结构体的构造函数生成block3个参数与结构体的成员变量对应。当block不持有临时变量时:

image.png

此时如果捕获的是对象类型呢?

IMG_6.png

可以看到仍然是对objc的赋值,这里变成了引用类型也就是持有。

  • block捕获值类型/引用类型的时候内部结构体生成了对应的成员变量,通过C++语法在构造函数中直接赋值。
  • 在编译阶段所有类型的blockisa都指向_NSConcreteStackBlock,也就是说block类型是在运行时确定的。

既然block在底层是一个结构体,而oc对象在底层也是结构体。那么block是否是oc的对象呢?

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

NSLog(@"%d",[block isKindOfClass:NSClassFromString(@"__NSGlobalBlock__")]);

输出:1。那么可以确定其实block也是oc对象。在源码中有如下定义:

image.png

block也是oc对象,继承自NSObject

3.2 __main_block_func_0

第一个参数fp(__main_block_func_0)赋值给了FuncPtrFuncPtr是在block调用的时候进行调用的。fp是函数式保存,这样就说明如果block不执行它的功能就不会被调用。
__main_block_func_0内部获取了结构体存储的值,对于值类型/引用类型以及__block修饰的类型的存储与调用如下:

image.png

  • 基本类型存储的是值,调用的是存储的这个值。
  • 引用类型存储的是指针,调用的是对应的指针。
  • __block修饰的类型存储的是__Block_byref_,调用的是ref->__forwarding->b

那么__block究竟干了什么呢?

3.2.1 基本类型的__block

mainb变成了__Block_byref_b_0(精简后的去掉了类型强转):

__Block_byref_b_0 b = {
    (void*)0,
    (__Block_byref_b_0 *)&b,//取b本来的的地址
    0,
    sizeof(__Block_byref_b_0),
    20};

__Block_byref_b_0结构如下:

struct __Block_byref_b_0 {
  void *__isa;
__Block_byref_b_0 *__forwarding;
 int __flags;
 int __size;
 int b;
};

这里看__forwarding就是指向b。在block的构造函数中赋值给b的是__forwarding也就是b的指针。所以加了__block后基本数据类型就指向了外部的变量,这个时候修改ref就是修改外部的基本类型了。

3.2.2 引用类型的 __block

objc2变成了__Block_byref_objc2_1类型:

__Block_byref_objc2_1 objc2 = {
    (void*)0,
    (__Block_byref_objc2_1 *)&objc2,//取objc2本来的地址,指向的内存空间
    33554432,
    sizeof(__Block_byref_objc2_1),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"))};

__Block_byref_objc2_1的结构如下:

struct __Block_byref_objc2_1 {
  void *__isa;
__Block_byref_objc2_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *objc2;
};

__forwarding指向objc2指向的内存空间。也就是__forwardingobjc2两个指针指向了同一片内存空间,不是之前的指向objc2了。所以就能在block内部修改objc2本身了。

  • __block生成了__Block_byref_结构体,传递给结构体的是对应指向的内存空间。

__Block_byref_objc2_1中还发现了__Block_byref_id_object_copy以及__Block_byref_id_object_dispose,对应的传递的函数为__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131(只有定义没有调用):

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

内部调用了_Block_object_assign_Block_object_dispose

3.3 __main_block_desc_0

block构造函数的第二个参数是__main_block_desc_0_DATA

image.png

  • reserved是保留字段。
  • Block_size block(__main_block_impl_0)的大小,实际上就是成员变量占用的内存空间。
  • copy拷贝函数,传递的是__main_block_copy_0
  • dispose释放函数,传递的是__main_block_dispose_0

__main_block_copy_0以及__main_block_dispose_内部对block持有的引用类型以及__block修饰的类型(ref)进行了_Block_object_assign_Block_object_dispose的调用,他们的实现并没有在.cpp文件中。

3.2 __weak

__weak为什么能解决循环引用呢?

NSObject *objc = [NSObject alloc];
__weak NSObject *objc2 = [NSObject alloc];
void(^block)(void) = ^{
    NSLog(@"%@,%@",objc,objc2);
};
block();

image.png

可以看到blockweak变量的持有也是__weak类型,调用依然是。也就是对应的 遇弱捕弱,遇强补强

四、汇编分析block底层

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

在有缓存的情况下可能会省略掉,最好使用一个新的工程测试。

4.1 定位到 _Block_copy

block的赋值下断点查看汇编调用:

image.png

下符号断点objc_retainBlock或者直接跟踪进去:
image.png

定位到了libobjc objc_retainBlock,在源码中它的实现如下:

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

内部调用了_Block_copy,它的实现并不在objc中:

image.png

继续对_Block_copy下符号断点:
image.png

定位到了libsystem_blocks.dylib,那也就是说block的实现是在libsystem_blocks中的。libsystem_blocks本身没有开源,可以通过libclosure工程替代。

可以将libclosure编译成可运行的工程调试。

4.2 汇编分析 _Block_copy

断点进入_Block_copy后读取寄存器查看参数:

image.png

这个时候是一个全局block,修改代码让block持有变量再次查看:
image.png

变成了栈block,也就是说在_Block_copy之前仍然是栈block。在汇编ret继续断点查看x0
image.png

  • block在编译阶段isa指针指向_NSConcreteStackBlock,在运行阶段调用_Block_copy之后从栈block变成了堆block

4.3 汇编分析 签名函数

在打印block的时候有一个signature,它是否v8@?0就是上面block的签名呢?

  • v返回值void
  • 8代表占用的空间。
  • @?代表一个类型,从0号位置开始占用8字节。
image.png
  • block的签名类型就是@?

五、libclosure 分析 block 底层实现

5.1 _Block_copy 分析

/*
 拷贝 block,参数是 block (Block_layout 对象)本身。
 1.堆上 block 引用计数 + 1。
 2.栈上 block 先拷贝到堆上,引用计数初始化为1。然后调用 copy helper 方法(如果存在)。
 3.全局 block 直接返回block本身。
 返回 拷贝后的 block 地址。
 */
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    //block 不存在,直接返回 NULL。
    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    //block 强转为 Block_layout 对象
    aBlock = (struct Block_layout *)arg;
    //已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        //引用计数 + 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    //全局 block 不进行操作直接返回。
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {//栈 block,需要拷贝到 堆上
        // Its a stack block.  Make a copy.
        //获取原始 栈block 大小
        size_t size = Block_size(aBlock);
        //在堆上重新开辟一块 与 aBlock 相同大小的内存空间
        struct Block_layout *result = (struct Block_layout *)malloc(size);
        // 开辟失败,返回 NULL
        if (!result) return NULL;
        //将 aBlock 内存上的数据全部复制到新开辟的 result 上
        memmove(result, aBlock, size); // bitcopy first
......
        // reset refcount
        //重新设置引用计数以及释放标志位(清除置为0)
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 将 result 标记位为堆上,需要手动释放;并且引用计数初始化为 1
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        //拷贝成员变量的工作 调用 Block_descriptor_2 以及 Block_descriptor_3
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        //isa标记为栈block
        result->isa = _NSConcreteMallocBlock;
        //返回新的block
        return result;
    }
}
  • 参数是blockBlock_layout 对象)本身。
  • BLOCK_NEEDS_FREE代表已经有引用计数了(也就是堆block),则引用计数+1
  • 全局block直接返回。
  • block创建空间拷贝block
    • 重置引用计数标志以及释放标识位。
    • 设置引用计数为1
    • 调用Blockcopy函数,内部调用的是_Block_object_assign(也就是拷贝拷贝成员变量)。
    • 设置isa标志为_NSConcreteMallocBlock
    • 返回拷贝后的 block 地址。

5.1.1 latching_incr_int

/*
 引用计数加 1,最多不超过 BLOCK_REFCOUNT_MASK。
 volatile的作用是:作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。简单地说就是防止编译器对代码进行优化
 */
static int32_t latching_incr_int(volatile int32_t *where) {
    while (1) {
        // 如果 old_value 在第 1~15 位都已经变为 1 了,即引用计数已经满了,就返回 BLOCK_REFCOUNT_MASK
        int32_t old_value = *where;
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return BLOCK_REFCOUNT_MASK;
        }
        // 比较 where的值 是否等于 old_value,如果等于,就将新值 oldValue + 2 放入 where
        // 否则继续下一轮循环
        // 这里加 2,是因为 flag 的第 0 位已经被占了,引用计数是第 1~15 位,所以加上 0b10,引用计数只是加 1
        if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
            return old_value+2;
        }
    }
}

引用计数加 1,最多不超过 BLOCK_REFCOUNT_MASK

5.1.2 _Block_call_copy_helper

/*
 调用 block 的 copy helper 方法,即 Block_descriptor_2 中的 copy 方法
 */
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    //获取 copy 函数
    if (auto *pFn = _Block_get_copy_function(aBlock))
        //调用函数,最终会调用到 _Block_object_assign
        pFn(result, aBlock);
}

获取Block_descriptor_2copy进行调用。

5.2 Block_layout 结构

_Block_copyblock是以Block_layout接收的,也就是说block在底层是Block_layout类型:

struct Block_layout {
    void * __ptrauth_objc_isa_pointer isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;//保留字段
    BlockInvokeFunction invoke;//最终回调
    struct Block_descriptor_1 *descriptor;//描述信息
    // imported variables
};
  • isa指针指向block对应类型。
void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };
  • flags标志位记录是否正在析构、引用计数掩码、是否有签名等。
  • invokeblock最终的回调函数。
  • descriptor记录描述信息。

// imported variables注释说明了应该还有其它字段。

5.2.1 flags 标志位说明

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime 
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime 引用计数掩码
    BLOCK_INLINE_LAYOUT_STRING = (1 << 21), // compiler

#if BLOCK_SMALL_DESCRIPTOR_SUPPORTED
    BLOCK_SMALL_DESCRIPTOR =  (1 << 22), // compiler
#endif

    BLOCK_IS_NOESCAPE =       (1 << 23), // compiler
    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位:是否拥有拷贝辅助函数(a copy helper function )。
  • 26位:是否拥有block析构函数。
  • 27位:标志是否有垃圾回收(macOS使用)。
  • 28位:标志是否是全局block
  • 30位:与BLOCK_USE_STRET相对,判断是否当前block拥有一个签名。用于 runtime运行时动态调用。

5.2.2 Block_descriptor_1

struct Block_descriptor_1 {
    uintptr_t reserved;//保留字段
    uintptr_t size;//大小
};

但是在上面clang分析的时候__main_block_desc_0不仅仅是这两个字段,并且汇编打印的时候block中也有其它字段,比如copydispose就没有出现在Block_layoutBlock_descriptor_1中。但是在Block_descriptor_1的周围发现了Block_descriptor_2Block_descriptor_3

image.png

也就是通过flags标志位BLOCK_HAS_COPY_DISPOSEBLOCK_HAS_SIGNATURE确定是否有Block_descriptor_2Block_descriptor_3的。

那么是在什么时机进行拼接生成的呢?
既然有设置就有获取,搜索_Block_descriptor_2的获取:

image.png

可以看到在desc1后面拼接了desc2以及desc3,当然这个过程中会根据标志位判断他们是否存在。
对应结构如下:
image.png

读取block内存结构如下:

image.png

block偏移8 + 4 + 4字节得到invoke(与打印出来的invoke地址相同),偏移8 + 4 + 4 + 8得到desc指针:
image.png

desc指针指向desc1desc2以及desc3中保存的值。与copydisposesignature是完全对应的。

desc根据标记位以及内存平移进行访问。

5.3 block捕获变量的生命周期

上面分析了Block_layout的内存结构,那么变量是如何捕获和释放的呢?
clang分析的时候__main_block_copy_0以及__main_block_dispose_赋值给了copy以及dispose

image.png

其实现调用了_Block_object_assign_Block_object_dispose。这里的copydisposedesc2中的,与block本身的copy没有关系。copy的是捕获的引用类型以及__block修饰的成员变量,

搜索_Block_object_assign有注释(翻译过后)如下:

 这是给编译器提供的 API
 
 block 可以引用 4 种不同的类型的对象,当 block 被拷贝到堆上时,需要 help,即帮助拷贝一些东西。
 1)基于 C++ 栈的对象
 2)Objective-C 对象
 3)其他 Block
 4)被 __block 修饰的变量
 
 block 的 helper 函数是编译器合成的(比如编译器写的 __main_block_copy_1() 函数),它们被用在 _Block_copy() 函数和 _Block_release() 函数中。copy helper 对基于 C++ 栈的对象调用调用 C++ 拷贝构造函数,对其他三种对象调用 _Block_object_assign 函数。 dispose helper 对基于 C++ 栈的对象调用析构函数,对其他的三种调用 _Block_object_dispose 函数。
 
 _Block_object_assign 和 _Block_object_dispose 函数的第三个参数 flags 有可能是:
 1)BLOCK_FIELD_IS_OBJECT(3) 表示是一个对象
 2)BLOCK_FIELD_IS_BLOCK(7) 表示是一个 block
 3)BLOCK_FIELD_IS_BYREF(8) 表示是一个 byref,一个被 __block 修饰的变量;如果 __block 变量还被 __weak 修饰,则还会加上 BLOCK_FIELD_IS_WEAK(16)
 
 所以 block 的 copy/dispose helper 只会传入四种值:3,7,8,24
 
 上述的4种类型的对象都会由编译器合成 copy/dispose helper 函数,和 block 的 helper 函数类似,byref 的 copy helper 将会调用 C++ 的拷贝构造函数(不是常拷贝构造),dispose helper 则会调用析构函数。还一样的是,helpers 将会一样调用进两个支持函数中,对于对象和 block,参数值是一样的,都另外附带上 BLOCK_BYREF_CALLER (128) bit 的信息。#疑问:调用的这两个函数是啥?BLOCK_BYREF_CALLER 里究竟存的是什么??
 
 所以 __block copy/dispose helper 函数生成 flag 的值为:对象是 3,block 是 7,带 __weak 的是 16,并且一直有 128,有下面这么几种组合:
    __block id                   128+3       (0x83)
    __block (^Block)             128+7       (0x87)
    __weak __block id            128+3+16    (0x93)
    __weak __block (^Block)      128+7+16    (0x97)

对应的有如下枚举定义:

// 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_FIELD_IS_BLOCK    =  7,  // a block variable
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};

所以block对捕获变量的类型有如下处理:

  • BLOCK_FIELD_IS_OBJECT:对象类型变量。
  • BLOCK_FIELD_IS_BLOCKblock类型变量。
  • BLOCK_FIELD_IS_BYREF__block类型变量。
  • BLOCK_FIELD_IS_WEAK__weak修饰的变量。
  • BLOCK_BYREF_CALLERref类型的变量。

使用以下代码进行验证:

NSObject *objc = [NSObject alloc];
void(^block1)(void) = ^{
    NSLog(@"%@",objc);
};
__block NSObject *objc2 = [NSObject alloc];
__weak NSObject *objc3 = [NSObject alloc];
__weak __block NSObject *objc4 = [NSObject alloc];
void(^block)(void) = ^{
    NSLog(@"%@,%@,%@,%@,%@",objc,block1,objc2,objc3,objc4);
};
block();

image.png

__weak底层是与ref相同的处理。

对于错误1.cannot create _weak reference because the current deployment target does not support weak references需要指定-fobjc-runtime版本。
2.cannot create __weak reference in file using manual reference需要指定-fobjc-arc
完整命令:
xcrun clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 block.m -o block.cpp

5.3.1 _Block_object_assign

//参数 destArg 是一个二级指针,指向真正的目标指针。
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:
        // _Block_retain_object_default = fn (arc),交给了系统arc处理。
        _Block_retain_object(object);
        //src 给到 dest 指向同一块内存。地址指向相同,地址不同。
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        //block会被传过来的block进行拷贝
        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        //__block 对象,也就是 ref 拷贝,使 dest 指向拷贝到堆上的 byref
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *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:
        *dest = object;
        break;

      default:
        break;
    }
}

根据传递的类型的不同进行不同的处理:

  • 普通对象类型(3):retain操作交给arc自动处理,然后指针赋值。
  • block类型(7):调用_Block_copy进行拷贝。
  • __block类型(8):调用_Block_byref_copyref进行拷贝,如果捕获的对象是引用类型则继续调用_Block_object_assign进行普通对象类型的拷贝。

5.3.2 _Block_byref_copy

/*
    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);
        //isa指向置为空
        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(src 的 forwarding 也指向了 copy,相当于引用了 copy)
        
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        //堆空间 forwarding 指向自己
        copy->forwarding = copy; // patch heap copy to point to itself
        //栈空间原先的对象 forwarding 指向 拷贝的堆空间。原始的 forwarding 与 拷贝的 forwarding 都指向 堆空间的。
        src->forwarding = copy;  // patch stack to point to heap copy
        //size大小赋值
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {//有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);
            //函数赋值
            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;
            }

            //byref_keep的调用(ref copy 的调用,最终也是调用了_Block_object_assign,走的是普通对象的逻辑)。
            (*src2->byref_keep)(copy, src);
        }
        else { //如果 src 没有 copy/dispose helper
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            // 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
            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) {
        // src 已经在堆上,就只将引用计数加 1
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}
  • 如果ref已经拷贝到堆区了则引用计数+1,否则进行拷贝。
  • 创建Block_byref的空间。
  • isa指向null
  • copyrefforwarding指向自己,原先栈空间的refforwarding指向copy的堆空间的ref
  • copydispose则创建ref_2(引用类型),赋值原先的copydispose函数(这里的copydispose是针对ref本身的)。
    • layout则创建ref_3并赋值。
    • 调用byref_keep也就是copy函数,最终会调用到_Block_object_assign,参数layoutobjc
  • 没有copydispose直接将ref_3拷贝(值类型)。

5.3.3 Block_byref

__block修饰的变量会被转换成Block_byref结构体:

image.png

Block结构体本身一样,它的一些字段也是可选的。
对应clang的赋值:
image.png

Block_ref内部的copy传递的参数是objc本身。

  • 调用_Block_copy拷贝自身,从 ->
  • _Block_copy中调用_Block_object_assign进行拷贝捕获的变量(引用类型&__block才有)。
    • 如果是普通引用类型走的是指针赋值的逻辑。
    • 如果是block_Block_copy逻辑。
    • __block_Block_byref_copy逻辑拷贝Block_byref
      • 引用类型会再走Block_byrefcopy逻辑。再次执行_Block_object_assign拷贝引用类型本身。

__block修饰的引用类型的三层copy

  • copy block本身,栈 -> 堆
  • block copy过程中copy Block_byref
  • Block_byref copy过程中copy objc

5.4 block 释放

既然有copy那么就有对应的释放,__block修饰的引用类型的变量被block捕获的时候有3copy,那么释放应该也是3层。

5.4.1 _Block_release

/*
 1.block 在堆上,才需要 release,在全局区和栈区都不需要 release.
 2.先将引用计数减 1,如果引用计数减到了 0,就将 block 销毁
 */
void _Block_release(const void *arg) {
    //强转
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    //block不存在直接返回。
    if (!aBlock) return;
    //全局 block 直接返回
    if (aBlock->flags & BLOCK_IS_GLOBAL) return;
    //栈区 block 直接返回
    if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
    // 引用计数减 1,如果引用计数减到了 0,会返回 true,表示 block 需要被销毁
    if (latching_decr_int_should_deallocate(&aBlock->flags)) {
        //调用block dispose
        _Block_call_dispose_helper(aBlock);
        //销毁实例
        _Block_destructInstance(aBlock);
        free(aBlock);
    }
}
  • block 在堆上,才需要 release,在全局区和栈区都不需要 release
  • 先将引用计数减 1,如果引用计数减到了 0,就将 block 销毁。
    • 先调用block dispose
    • 销毁block实例,释放空间。

5.4.2 latching_decr_int_should_deallocate

/*
 1.引用计数减 1,如果引用计数减到了 0,就将 block 置为 deallocating 状态
 2.返回值是 block 是否需要被 dealloc
 */
static bool latching_decr_int_should_deallocate(volatile int32_t *where) {
    while (1) {
        int32_t old_value = *where;
        // 如果引用计数还是满的,就不能 dealloc,#疑问:引用计数满了以后就不能减了么?满了后永远不释放了?
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return false; // latched high
        }
        // 如果引用计数为 0,按照正常的逻辑,它应该已经被置为 deallocating 状态,不需要再被 dealloc,所以返回 false
        if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
            return false;   // underflow, latch low
        }
        //引用计数减 1
        int32_t new_value = old_value - 2;
        bool result = false;
        // 如果 old_value 在 0~15 位的值是 0b10,即引用计数是 1,且不是 deallocating 状态
        if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) {
            // old_value 减 1 后,新值 new_value 在 0~15 位值是 0b01,即引用计数变为0,且 BLOCK_DEALLOCATING 位变为 1
            new_value = old_value - 1;
            // 需要被 dealloc
            result = true;
        }
        // 将新值 new_value 放入 where 中
        if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
            return result;
        }
    }
}
  • 引用计数减 1,如果引用计数减到了 0,就将 block 置为 deallocating 状态。
  • 返回值是BOOL值,确定block是否需要被 dealloc

5.4.3 _Block_object_dispose

/*
 调用 block 的 dispose helper 方法,即 Block_descriptor_2 中的 dispose 方法
 */
static void _Block_call_dispose_helper(struct Block_layout *aBlock)
{
    //找到dispose
    if (auto *pFn = _Block_get_dispose_function(aBlock))
        //调用dispose,最终会调用到 _Block_object_dispose
        pFn(aBlock);
}

_Block_call_dispose_helper最终会调用到_Block_object_dispose

image.png

blockdispose也就是调用_Block_object_dispose释放持有的对象以及ref

_Block_object_dispose

//当 block 和 byref 要 dispose 对象时,它们的 dispose helper 会调用这个函数
void _Block_object_dispose(const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      // byref
      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
        //ref 调用 _Block_byref_release,对 byref 对象做 release 操作
        _Block_byref_release(object);
        break;
      case BLOCK_FIELD_IS_BLOCK:
        // block 继续调用 _Block_release,block 做 release 操作
        _Block_release(object);
        break;
      case BLOCK_FIELD_IS_OBJECT:
        //对象交给ARC
        _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;
    }
}
  • 普通对象类型(3):release操作交给arc自动处理。
  • block类型(7):调用_Block_release进行释放。
  • __block类型(8):调用_Block_byref_releaseref进行释放,如果捕获的对象是引用类型则继续调用_Block_object_dispose进行普通对象类型的释放。

5.4.4 _Block_byref_release

/*
 对 byref 对象做 release 操作。
 1.堆上的 byref 需要 release,栈上的不需要 release。
 2.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;//获取 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) {
                //dispose函数获取
                struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                //调用 _Block_object_dispose 释放objc
                (*byref2->byref_destroy)(byref);
            }
            //释放ref
            free(byref);
        }
    }
}
  • 堆上的 byref 需要 release,栈上的不需要 release
  • 堆上byref释放操作:
    • 取得引用计数。
    • 引用计数减 1,如果引用计数减到了 0,会返回 true,表示 byref 需要被销毁。
    • 如果是引用类型则调用refdispose函数,内部其实调用了_Block_object_dispose参数是objc
      • _Block_object_dispose这次走普通对象逻辑。
    • 调用free释放空间。

__block修饰的引用类型的三层disposecopy完全相反:

  • Block_byref dispose过程中先dispose objc
  • block dispose过程中先dispose Block_byref
  • dispose block本身,释放空间

六、总结

  • block 分类:
    • 全局 block 位于全局区,在 Block内部不使用外部变量,或者只使用静态变量和全局变量。
    • 堆 block 位于堆区,在block内部使用局部变量或者OC属性,并且赋值给强引用或者copy修饰的变量。
    • 栈 block 位于栈区,与堆block一样,可以在内部使用局部变量或者OC属性,但是不能赋值给强引用或者copy修饰的变量。
  • block 拷贝到 堆 block的情况(捕获了局部变量或者OC属性):
    • 手动copyblock作为返回值、被强引用或者copy修饰、系统API包含usingBlock
  • block捕获变量(遇强捕强、遇弱捕弱):
    • block捕获变量引用计数+1,堆block捕获变量引用计数+2(堆栈各+1)。
    • __weak修饰的比变量会被加入弱引用表,在调用CFGetRetainCount的时候会临时对引用计数+1,调用完后会释放-1
    • block捕获对象是指针引用,捕获基本数据类型是值拷贝。栈block捕获的对象类型在出了block作用域后对象就被释放了。
    • weak或者assign修饰的block在出了函数栈后block被释放,为空;在出了作用域后捕获的对象被释放,为空。
    • block的捕获是 逐层捕获
  • block从栈中copy到堆中是 深拷贝,堆中block拷贝是引用计数增加。
  • 栈的作用域在函数栈区,堆的作用域在代码块中。栈block作用域同理。
  • __weak 是将修饰的变量放入弱引用表,用的时候从弱引用表取出。取的时候如果为nil了就赋值nil,否则就赋值它所指向的对象给临时变量,临时变量出了作用域后自动释放。这也就是weak能解决循环引用的根本原因(block捕获的变量也是weak类型)。
  • 循环引用的解决方案:
    • weak-strong-dance(强弱共舞)。
    • 临时变量置nil
    • self 作为 block 参数。
  • block在底层是结构体,有一个isa指针以及funcptrfuncptr指向一个函数(代码块函数)。block()执行相当于block调用funcPtr参数是block自己。
  • block捕获值类型/引用类型的时候内部结构体生成了对应的成员变量。基本类型存储的是值,调用的是存储的这个值。引用类型存储的是指针,调用的是对应的指针。
  • block也是oc对象,继承自NSObject
  • __block修饰的类型存储的是__Block_byref_结构体,传递给结构体的是对应指向的内存空间。
  • block在编译阶段isa指针指向_NSConcreteStackBlock,在运行阶段调用_Block_copy之后从栈block变成了堆block
  • block的签名类型是@?
  • block 的拷贝(_Block_copy):
    • 1.堆上 block 引用计数 + 1。
    • 2.栈上 block 先拷贝到堆上,引用计数初始化为 1。然后调用 copy helper 方法(如果存在,最终会调用到_Block_object_assign拷贝成员变量)。
      • 2.1 普通对象类型(3),retain操作交给arc自动处理,然后指针赋值。
      • 2.2 block类型(7),调用_Block_copy进行拷贝。
      • 2.3 __block类型(8),调用_Block_byref_copyref进行拷贝。
        • 2.3.1 ref已经在堆区则引用计数+1,否则进行ref拷贝。
        • 2.3.2 ref是引用类型(有copy以及dispose)则继续调用_Block_object_assign,参数是objc
    • 3.全局 block 直接返回block本身。
    • 4.__block修饰的引用类型的三层 copy:
      • copy block本身,栈 -> 堆。
      • block copy过程中copy Block_byref
      • Block_byref copy过程中copy objc
  • block 的释放(_Block_release):
    • 1.block 在堆上,才需要 release,在全局区和栈区都不需要 release
    • 2.先将引用计数减 1,如果引用计数减到了 0,就将 block 销毁。
      • 2.1 先调用block dispose_Block_object_dispose)。
        • 2.1.1 普通对象类型(3),release操作交给arc自动处理。
        • 2.1.2 block类型(7),调用_Block_release进行释放。
        • 2.1.3 __block类型(8),调用_Block_byref_releaseref进行释放,如果捕获的对象是引用类型则继续调用_Block_object_dispose进行普通对象类型的释放。
          • 堆上的 byref 需要 release,栈上的不需要 release
          • ref引用计数-1,如果引用计数减到了0,会返回 true,表示 ref 需要被销毁。
          • 如果ref持有的是引用类型则调用refdispose函数,内部其实调用了_Block_object_dispose参数是objc
          • 释放空间。
      • 2.2 销毁block实例,释放空间。
    • __block修饰的引用类型的三层disposecopy完全相反:
      • 先释放ref持有的objc
      • 释放Block_byref
      • 释放block本身,释放内存空间。
  • strongcopy修饰block最终都会调用_Block_copy,只不过strong修饰的会先调用objc_retainBlock

你可能感兴趣的:(block 底层原理)