美文网首页
block的本质

block的本质

作者: 知之未道 | 来源:发表于2021-08-12 10:51 被阅读0次

    Block结构

    block本质上也是一个OC对象,它内部也有个isa指针
    block是封装了函数调用以及函数调用环境的OC对象
    block的底层结构如图所示

    block的底层结构.png

    声明一个block转义之后是下面这种代码

    ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, 
                                    &__test_block_desc_0_DATA, 
                                     ));
    主要的方法,就是__test_block_func_0结构体的的内容 和 __test_block_desc_0_DATA结构体的描述 
    

    block的结构

    struct __test_block_impl_0 {
      struct __block_impl impl;
      struct __test_block_desc_0* Desc;
      int a;
      int *b;
      __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    传入的参数会在自己的构造函数 __test_block_impl_0 中用到,这个构造函数类似于 init 方法,返回的是一个结构体
    block的ISA指向的是_NSConcreteStackBlock类型,相当于,对象指向的是NSObject类型一样
    

    block大括号里面的实现代码:

    static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
      int *b = __cself->b; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_fd2a14_mi_0, a, (*b));
    }
    

    结构体的描述

    static struct __test_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)}; 
    

    block的调用

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

    相当于调用了block结构体重的impl对象的FuncPtr方法,但是一般是impl. FuncPtr才可以,这个却能直接调用呢,是因为将__test_block_func_0的类型直接转换成了 void (*)(__block_impl * 类型,impl 是 __test_block_func_0 结构体的第一个成员变量,所以 impl 的地址是和 __test_block_func_0 的地址一样的,所以才能转换类型成功

    Block捕获器

    局部变量分为auto和static,auto是默认方法,比如 int a = 10,其实全称应该是 auto int a = 10

    Block捕获.png

    当定义一个方法的时候,调用的时候相当于默认传递了两个参数,一个是当前对象,一个是方法名称,所以无论是在Block中使用self还是self的某个属性都会捕获当前对象

    auto变量的捕获.png
    - (void)test
    {
        void (^block)(void) = ^{
            NSLog(@"-------%d", [self name]);
        };
        block();
    }
    

    Block类型

    block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

    NSGlobalBlock ( _NSConcreteGlobalBlock )
    NSStackBlock ( _NSConcreteStackBlock )
    NSMallocBlock ( _NSConcreteMallocBlock )

    Block的类型所在内存区域如下图:


    Block的类型.png
    block类型 环境
    NSGlobalBlock 没有访问auto变量
    NSStackBlock 访问了auto变量
    NSMallocBlock NSStackBlock调用了copy
    block类型 副本源的配置存储域 复制效果
    NSGlobalBlock 程序的数据区域 什么也不做
    NSStackBlock 从栈复制到堆
    NSMallocBlock 引用计数加一

    Block的copy

    在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况,block作为函数返回值时,将block赋值给__strong指针时,block作为Cocoa API中方法名含有usingBlock的方法参数时,block作为GCD API的方法参数时

    MRC下block属性的建议写法

    @property (copy, nonatomic) void (^block)(void);
    

    ARC下block属性的建议写法

    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);
    

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

    如果block被拷贝到堆上
    会调用block内部的copy函数
    copy函数内部会调用_Block_object_assign函数
    _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

    如果block从堆上移除
    会调用block内部的dispose函数
    dispose函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放引用的auto变量(release)

    将Block从栈copy到堆中,desc中会多出两个结构体

    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 
        0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0
    };
    
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    

    下面这种情况下,可以看到最里面是有用到强引用的p的,这个时候编译器会全部考虑进去,最里面有p,所以当1秒之后并不会释放p对象

    MJPerson *p = [[MJPerson alloc] init];
        
    __weak MJPerson *weakP = p;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1-------%@", weakP);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-------%@",p);
        });
    });
    

    在Block中修改变量

    方法一

    设置为全局变量,或者是使用static声明变量

    static age = 10;
    MJBlock block2 = ^{
        age = 30;
        NSLog(@"age is %d", age);
    };
    

    方法二

    使用__block修饰,__block可以用于解决block内部无法修改auto变量值的问题,__block不能修饰全局变量、静态变量(static),编译器会将__block变量包装成一个对象

    __block int age = 10;
    MJBlock block1 = ^{
        age = 20;
        NSLog(@"age is %d", age);
    };
    

    通过__block修饰后,会将 int age 修改为__Block_byref_age_0 *age;结构体如下,证明也是一个对象

    struct __Block_byref_age_0 {
      void *__isa;
    __Block_byref_age_0 *__forwarding;//指向自己,就是 &age
     int __flags;
     int __size;
     int age;
    };
    
    __Block_byref_age_0.png

    对象使用的时候并不需要加 __block,但是当修改的对象的值的时候是需要加的

    NSMutableArray *array = [NSMutableArray array];
            
    MJBlock block1 = ^{
        array = nil;  //报错
        [array addObject:@"123"];  //不报错
    };
    

    当我们在外部定义一个变量的时候,在block中修改,这时候这个变量就是 __block生成的结构体里面的变量地址

    __block int age = 10;
            
    MJBlock block = ^{
        age = 20;
        NSLog(@"age is %d", age);
    };
        
    struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
        
    NSLog(@"%p", &age);  //这个地址就是__Block_byref_age_0 里面的那个变量地址
    

    当__block变量在栈上时,不会对指向的对象产生强引用

    当__block变量被copy到堆时,会调用__block变量内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用<font color="#FF0033">(注意:这里仅限于ARC时会retain,MRC时不会retain)</font>

    如果__block变量从堆上移除
    会调用__block变量内部的dispose函数
    dispose函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放指向的对象(release)

    Block内存管理

    Block内存管理.png

    当block在栈上时,并不会对__block变量产生强引用

    当block被copy到堆时,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(retain)

    当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的__block变量(release)

    block从堆中移除.png

    当block在栈上时,对它们都不会产生强引用

    当block拷贝到堆上时,都会通过copy函数来处理它们

    __block变量(假设变量名叫做a)
    _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    

    对象类型的auto变量(假设变量名叫做p)

    _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
    

    当block从堆上移除时,都会通过dispose函数来释放它们

    __block变量(假设变量名叫做a)
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    

    对象类型的auto变量(假设变量名叫做p)

    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
    

    __block的__forwarding指针

    __forwarding.png

    在block中修改值

    __block int age = 10;
            
    MJBlock block = ^{
        age = 20;
        NSLog(@"age is %d", age);
    };
    

    实现是这样的,先拿到 &age的地址,也就是 __Block_byref_age_0 结构体,然后拿到里面的 __forwarding指针,__forwarding指向自己,然后在拿到 __Block_byref_age_0 结构体里面的 age,为什么通过 __forwarding指针 找age是因为,使用的时候, block被强引用的时候会被copy到堆上,__forwarding的作用就是为了保证指向正确的那个地址,在堆上指向copy到堆上的地址,在栈上指向栈上的地址

    __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
    
    (age->__forwarding->age) = 20;
    

    Block的循环引用

    导致循环引用的原因是相互持有对方,比如下面代码,person使用了强引用,然后又在block中使用了person,就导致了循环引用


    Block的循环引用.png
    MJPerson *person = [[MJPerson alloc] init];
            
    person.age = 10;
    person.block = ^{
        NSLog(@"age is %d", person.age);
    };
    

    解决方式(ARC)

    用__weak、__unsafe_unretained、__block解决

    __weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil

    __weak typeof(person) weakPerson = person;
    

    __unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变

    __unsafe_unretained typeof(person) weakPerson = person;
    

    __block的情况比较复杂,相当于是造成了一个三方的相互持有,对象->block->__block->__block内部的对象,但是我们需要手动调用下,block并且将对象置为Nil,这样就可以打破相互引用

    __block MJPerson *person = [[MJPerson alloc] init];
            
    person.age = 10;
    person.block = ^{
        NSLog(@"age is %d", person.age);
        person = nil;
    };
        
    person.block();
    
    解决方式(ARC).png

    解决方式(MRC)

    不支持__weak,只能使用__unsafe_unretained、__block解决

    __unsafe_unretained typeof(person) weakPerson = person;
    

    MRC的环境下__block 不会retain,所以使用方法和__unsafe_unretained一样

    相关文章

      网友评论

          本文标题:block的本质

          本文链接:https://www.haomeiwen.com/subject/bnbfbltx.html