ios block

1. block的分类

1. NSGlobalBlock
我们通常把内存分为五大区,堆区栈区全局静态区常量区代码区,当我们定义一个普通的block函数:

    void (^block)(void) = ^{
        
    };

    NSLog(@"%@",block);

通过打印

2021-02-18 09:57:19.402854+0800 001---Block[2105:42395] <__NSGlobalBlock__: 0x10dc4c088>

得出,block当没有捕获外界变量的时候是存放在全局静态区的。

2. NSMallocBlock
因为block拥有捕获外部变量的能力,当block捕获到外部变量时,就会由__NSGlobalBlock__变为__NSMallocBlock__

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

    NSLog(@"%@",block);

输出:

2021-02-18 10:01:48.334872+0800 001---Block[2259:49362] <__NSMallocBlock__: 0x60000184e9a0>

3. NSStackBlock

在定义block时,我们通常用copy或者strong去修饰,因为在copy操作之前,block存放在栈区,通过copy我们可以将block拷贝到堆区

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

    NSLog(@"%@",block);

输出:

2021-02-18 10:04:48.424646+0800 001---Block[2309:51300] <__NSStackBlock__: 0x7ffeec37a458>

block没有捕获外界变量时,是存放在全局区的,当我们对block强引用时,block存放在堆区,进行弱引用时存放在栈区。

2. 解决block循环引用

typedef void(^KCBlock)();
@interface ViewController ()
@property (nonatomic, copy) KCBlock block;
@property (nonatomic, copy) NSString *name;

@property (nonatomic, strong) UITableView *tableView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 循环引用
    self.name = @"jeffery";
    self.block = ^(void){
           NSLog(@"%@",self.name);
    };
    self.block();
}

1. __weak
由上述代码可知,self持有了blockblock里又持有了self,因此造成了循环引用,导致self无法释放。通常我们都会用__weak修饰self,blockself进行了弱引用,这样打破了双方的循环引用。但是通常在我们的业务逻辑中远没有这么简单,我们可能在block内进行了一些业务代码的逻辑处理,如果只是简单的进行弱引用,可能导致在释放的时候所持有的对象已没了,这样,self的生命周期是无法得到保证的,所以我们会在里面再进行一次强持有,来保证其生命周期。

      __weak typeof(self) weakSelf = self;
      self.block = ^(void){
          __strong __typeof(weakSelf)strongSelf = weakSelf; // 可以释放 when
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              NSLog(@"%@",strongSelf.name);
          });
      };
      self.block();

2. 中介者模式

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

因为通过__block修饰的对象可以被block捕获, 并且具有修改的能力。我们把self赋值给vc时,vc就具有了修改等能力。因此,在我们对vc进行了强持有后通过手动将vc置为nil,当前block也就没有对vc有持有关系,也就打破了循环引用。

3. 传值

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

直接将self以参数的形式传入block ,blockself没有了持有关系,也就打破了循环引用。

3. block是如何捕获外部变量的

3.1 block是什么

创建一个block.c文件:

#include "stdio.h"

int main(){

    void(^block)(void) = ^{

        printf("jeffery");
    };
    
 block();
    return 0;
}

通过clang -rewrite-objc block.c -o block.cpp命令,导出一份block.cpp文件。

ios block_第1张图片
main().png

通过底层探究可以得出^{}在底层为__main_block_impl_0这样一个函数,因为^{}没有命名,所以block为匿名函数。然后再文件中全局搜索__main_block_impl_0,发现__main_block_impl_0是一个结构体,这也是block可以用%@打印出来的原因,也间接说明了block本质也是一个对象。

ios block_第2张图片
__main_block_impl_0.png

ios block_第3张图片
funcPtr.png

我们看到 printf("jeffery");__main_block_func_0方法里被打印出来,在 block的构造函数里 impl.FuncPtr = fp;这一步进行了赋值,也就是函数被当成参数传了进来,这样就可以在需要的地方进行调用。

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

去掉强转类型得到block->FuncPtr(block),这里是block()的底层实现,这也是block必须进行调用的原因。

3.2 自动捕获外界变量

#include "stdio.h"

int main(){

    int age = 18;
    
    void(^block)(void) = ^{

        printf("jeffery = %d",age);
    };
    
    block();
    
    return 0;
}

我们对之前的block.c进行了小修改,依然通过clang命令编译出block.cpp文件。

ios block_第4张图片
捕获外界变量__main_block_impl_0 .png

通过编译发现, block在编译期间就在内部生成了相应的变量 int age,此时 isa赋值为 &_NSConcreteStackBlock,而我们之前通过打印出来的为 NSMallocBlock,可以得出,当 block捕获到外界变量时,在编译期存在于栈区,运行时存在于堆区。
ios block_第5张图片
__main_block_func_0赋值.png

我们在 block调用代码里看到有一步赋值操作: int age = __cself->age;,很显然,这里的 age与我们自己在 main函数里定义的不是同一个变量。假如,我们在 block进行了 age的一些赋值操作的话,肯定会编译失败。这里的 age只具备只读的功能。

__block
int age = 18;前使用__block修饰一下,然后重新进行clang编译。

ios block_第6张图片
(__block修饰)__main_block_impl_0.png

通过编译发现,使用 __block修饰的 age不是 int类型,而是为 __Block_byref_age_0类型的指针。全局搜索发现 __Block_byref_age_0为结构体类型。
ios block_第7张图片
__Block_byref_age_0.png

main函数里也多了一步结构体的赋值。
ios block_第8张图片
__Block_byref_age_0赋值.png

我们在代码里进行 age++操作,再进行 clang编译。
ios block_第9张图片
__main_block_func_0.png

通过底层,我们可以看到这里是进行了指针的拷贝,因此这里的 age与外界是同一个地址,然后我们就可以进行 age++的操作。

总结:
通过__block修饰的变量在底层编译时,会生成相应的结构体,保存了原来的指针和值,然后把指针的地址传给了block进行捕获。

4. block源码解析

在源码中打开Block_private.h文件,可以看到block在源码中的形式为Block_layout的结构体。

ios block_第10张图片
Block_layout.png

isa:指向当前的block类型,通过isa判断是栈区,堆区还是全局区;
flag:标志位;

ios block_第11张图片
flag.png

ios block_第12张图片
Block_descriptor_1&2&3.png

因为Block_descriptor_1是存在于block中,而Block_descriptor_2Block_descriptor_3是可选参数,我们通过flag这个标志位判断当前是Block_descriptor_2还是Block_descriptor_3。打开runtime.cpp:

ios block_第13张图片
Block_descriptor_2和3取值.png

通过上述代码可以看到, Block_descriptor_2Block_descriptor_3都是 flag&标志位来判断是否存在,如果不存在,直接返回NULL,如果存在,则通过内存平移得到当前的 Block_descriptor_2Block_descriptor_3

4.1 block_copy

ios block_第14张图片
block_copy.png

首先对传入进来的 block进行强转,然后通过标志位 flag进行是否正在进行释放的判断,如果是则进行一些处理然后返回,然后判断是否为 全局block,如果是 全局block则没有必要进行拷贝,直接返回。因为在编译期间 block不可能存放在堆区,所以只能是 栈区block。然后便是对栈区block进行拷贝操作。首先申请开辟内存,然后进行 memmove内存拷贝,然后判断是否将 invoke也拷贝进来,然后再进行一些标志位的处理,将 isa指向 堆区block,处理完成将结果返回。

3. block 捕获外界变量过程

在底层进行clang的时候,我们看到__main_block_copy_0__main_block_copy_0函数和__main_block_dispose_0分别调用了_Block_object_assign函数和_Block_object_dispose,并在里面引用了使用__block修饰的外部变量,这不禁让我记起了在探究block时的Block_descriptor_2的结构,当我们使用block_copy的时候使用的是Block_descriptor_2,所以这2个函数的调用应该是来自于Block_descriptor_2

ios block_第15张图片
block_assign&block_dispose.png

ios block_第16张图片
外部变量类型.png

因为我们常用普遍的大部分是 BLOCK_FIELD_IS_OBJECT(普通的对象)BLOCK_FIELD_IS_BYREF(被block修饰的变量),所以主要研究这2个。
ios block_第17张图片
_Block_object_assign.png

通过在 clang我们将参数进行比较,第一二个参数对应的是我们拿到的外部变量。当是普通的对象类型时,先进行 _Block_retain_object函数调用:
_Block_retain_object

_Block_retain_object_default.png

通过源码发现里面什么也没做,然后交给系统ARC进行持有,将当前对象进行指针拷贝,这样就有了2个地址指向了同一片内存空间,这样使得这片内存空间无法正常释放。

static struct Block_byref *_Block_byref_copy(const void *arg) {
    
    // Block_byref  结构体
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        
        // 问题 - block 内部 持有的 Block_byref 所持有的对象 是不是同一个
        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
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            (*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));//内存偏移得到Block_byre_3
        }
    }
    // 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修饰的对象时,进行了_Block_byref_copy函数调用。在clang的时候,我们已经知道被__block修饰的对象在底层都会被编译成Block_byref结构体。所以当外部变量传进来时,进行保存得到src。然后定义一个Block_byref的结构体copy进行内存申请,变量赋值。经过clang可知,在Block_byref中,forwarding指针对应的是外界变量的地址,在这里进行copysrc的赋值,让外部变量和block内部变量持有的对象相等了。这也是经过__block修饰的是同一片内存空间的原因。

ios block_第18张图片
Block_byref_2.png

ios block_第19张图片
Block_byref_2.png

接下来继续判断是否含有 copy操作,如果有,进行拷贝操作,再调用 byref_keep函数。
注:在这里因为外部变量是__block修饰的int类型,所以只进行了block本身的拷贝和Block_byref的拷贝,如果外部变量是__block修饰的对象类型时,还要对对象再进行一次拷贝,放到block本身的内存中。

ios block_第20张图片
_Block_object_dispose.png

当前类型如果是普通的类型,则调用系统ARC的释放,如果当前类型是 __block修饰的对象时,调用 _Block_byref_release函数进行释放。
ios block_第21张图片
_Block_byref_release.png

你可能感兴趣的:(ios block)