美文网首页
Block复习

Block复习

作者: 我才是臭吉吉 | 来源:发表于2019-08-22 16:39 被阅读0次

    1. Block的基本结构

    void (^testBlock)(void) = ^{
        NSLog(@"臭吉吉~");
    };
    testBlock();
    

    将包含Block的代码通过clang转换为c++代码(只用了c++的扩展struct,实际上还是c)。我们一句一句看:

    1. Block变量的声明:
    void (*testBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    

    可以看到,testBlock变量,实际上是 __main_block_impl_0 结构体实例的指针

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

    其中,用以识别Block对象的类型信息和Block的函数体都在 __block_impl 结构体中声明:

    struct __block_impl {
        // 类型
        void *isa;
        // 引用计数等会存在这里
        int Flags;
        // 保留位
        int Reserved;
        // 函数指针
        void *FuncPtr;
    };
    

    其中,FuncPtr指向的就是我们在Block中提供的函数体。而isa,即作为描述Block类型使用。由于Block在堆中也是遵循类似自动引用计数的内存管理机制,故可以把Block看做为对象。

    而Block的描述信息,则是指向全局的 __main_block_desc_0 结构体的实例。

    static struct __main_block_desc_0 {
        // 保留位
        size_t reserved;
        // Block整体的内存占用
        size_t Block_size;
    } __main_block_desc_0_DATA = { 
        0, 
        sizeof(struct __main_block_impl_0)
    };
    
    1. Block的执行
    ((void (*)(__block_impl *))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock);
    

    了解了Block的结构,这一句就很好理解了。由于testBlock的地址与 __block_impl 指针的地址相同,因此直接转换为 __block_impl 类型。然后,获取其中的 FuncPtr 函数指针,传入自身作为参数后,直接调用执行。

    传入自身作为 FuncPtr 的参数的目的

    由于Block的函数体在编译后成为了全局静态c函数(无状态保存)。因此,为了在调用时可以正常访问到捕获的变量,则将自身实例作为参数传入(这与OC调用方法的传参目的一样)。

    2.Block捕获的变量

    2.1 没有捕获变量

    Block在没有捕获任何变量时,其类型(isa)为NSGlobalBlock

    2.2 捕获基本类型变量

    测试代码:

    NSInteger value = 3;
    void (^testBlock)(void) = ^{
        NSLog(@"%ld", value);
    };
    testBlock();
    

    在运行时,此Block的类型为NSMallocBlock。已经被copy到堆中。

    对于基本数据类型的变量,捕获后,其值直接保存到 __main_block_impl_0 结构体中:

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        
        // 直接保存值
        NSInteger value;
        
        
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _value, int flags=0) : value(_value) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
      }
    };
    

    由于是值传递,直接修改此Block变量中的value是不会影响原value的值。因此,编译器则直接不允许修改捕获的变量。

    而且,这也解释了为何在 FuncPtr 中的需要传入block自身作为参数:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        // 通过自身取出捕获的变量
        NSInteger value = __cself->value;
    
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_zmhjsn0s5szgbxdxfqvbgqlc0000gn_T_main_cf26fa_mi_0, value);
            }
    
    2.3 捕获对象类型变量

    测试代码:

    id obj = [[NSObject alloc] init];
    void (^testBlock)(void) = ^{
        NSLog(@"%@", obj);
    };
    testBlock();
    

    在运行时,此Block的类型为NSMallocBlock。已经被copy到堆中。

    由于捕获的是对象类型,因此编译后的c++代码与刚才有些不同:

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        
        // 直接保存对象(也就是地址)
        id obj;
        
    
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _obj, int flags=0) : obj(_obj) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
      }
    };
    

    核心结构还是一样,直接将捕获对象保存到了 __main_block_impl_0 结构体中。产生变化的,是 __main_block_desc_0 的结构:

    static struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;
        
        // Block被copy时,捕获的变量执行的copy函数
        void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
        
        // Block释放时,捕获的变量执行的释放函数
        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
    };
    

    由于捕获的变量是对象类型,因此,需要在结构体中指定实现内存管理方式的相应实现(clang可以在Block的相关结构体中对OC对象进行内存管理,但需要提供相应实现)。

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    

    也就是说,当Block变量被copy到堆上时,系统则会调用 _Block_object_assign 函数,对捕获的obj进行retain;而当堆上的Block变量被释放时,系统则会调用 _Block_object_dispose 函数,对捕获的obj进行release操作。

    为了行为一致,编译器也不允许对捕获的对象类型变量进行修改。

    这可以保证捕获的对象在超出自身作用域后,继续生存(因为已经被堆上的Block保留)。

    2.4 捕获__block修饰的基本类型变量

    测试代码:

    __block NSInteger value = 3;
    void (^testBlock)(void) = ^{
        value -= 1;
    };
    testBlock();
    NSLog(@"%ld", value);
    

    首先,还是可以确认的是,在运行时,Block的类型是NSMallocBlock

    转换代码后,就可以看到,使用了 __block 修饰符的实现就变了很多。我们还是一句一句来看:

    __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {
        (void*)0,
        (__Block_byref_value_0 *)&value, 
        0, 
        sizeof(__Block_byref_value_0), 
        3
    };
    

    可以看到,__block 修饰的变量,实际上是一个全局的 __Block_byref_value_0 结构体的实例。我们看一下此结构体的内容:

    struct __Block_byref_value_0 {
        // 类型标识
        void *__isa;
        // 指向自身实例的指针
        __Block_byref_value_0 *__forwarding;
        
        int __flags;
        int __size;
        
        // 真正的值
        NSInteger value;
    };
    

    可以看到,原始变量的真实值保存在结构体中。此结构体中不仅包含了类型标识、尺寸等信息,还包含了一个指向自身实例的指针。

    下面是Block变量声明,只是将 __Block_byref_value_0 的地址传入,没有什么异常:

    // testBlock变量声明及赋值
    void (*testBlock)(void) = ((void (*)())&__main_block_impl_0(
        (void *)__main_block_func_0, 
        &__main_block_desc_0_DATA, 
        (__Block_byref_value_0 *)&value, 
        570425344)
    );
    
    
    // __main_block_impl_0的结构体
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        
        // 引用传递捕获的变量
        __Block_byref_value_0 *value;
        
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
      }
    };
    

    可以看到,唯一的区别就是,在捕获的带有 __block 修饰的变量,生成的Block变量中,是以引用传递的方式进行储存的。这也就意味着捕获的变量的内容是可以随意修改的,而且,访问或者修改的是 __Block_byref_value_0 的实例,而不是原始的变量

    对于Block中的描述信息,其实现也有些许变化:

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

    可以看到,使用 __block 修饰的变量,在捕获到Block中后,也需要在Block被copy到堆上、或从堆中释放时提供对应的内存管理函数。

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    

    这里与捕获对象类型变量时,生成的内存管理函数中,区别只是类型不同,是 BLOCK_FIELD_IS_BYREF (捕获的对象类型变量是 BLOCK_FIELD_IS_OBJECT

    与对象的保留关系不同,这种方式,实际上是创建一个新对象(结构体实例,如 __Block_byref_value_0 ,内部包含着被捕获的变量的值)直接存储在Block中。当Block被copy到堆上时,再创建一个新的 __Block_byref_value_0 实例,并保存在堆上的Block中

    __Block_byref_value_0 的结构中,为什么会包含一个指向自身实例的指针 __forwarding

    为了保证访问到捕获变量的一致性。
    在Block被copy到堆上时,不仅生成一个新的 __Block_byref_value_0 实例。而且将原始 __Block_byref_value_0__forwarding 指针指向了新的实例。因此,通过形如 value.__forwarding->value 的方式,不管是在栈上,还是在堆上,都可以访问到堆中的同一个变量。

    所以,我们最后看一下在Block执行之后,打印语句NSLog。

    NSLog(
        (NSString *)&__NSConstantStringImpl__var_folders_z5_zmhjsn0s5szgbxdxfqvbgqlc0000gn_T_main_316d85_mi_0, 
        (value.__forwarding->value)
    );
    

    由于是在栈上执行,因此 value.__forwarding->value 最终指向的是堆上的Block中的新 __Block_byref_value_0 实例。

    2.5 捕获__block修饰的对象类型变量

    测试代码:

    __block id obj = [[NSObject alloc] init];
    void (^testBlock)(void) = ^{
        obj = [[NSMutableArray alloc] init];
    };
    testBlock();
    NSLog(@"%@", obj);
    

    转换后的代码与 __block 修饰的基本类型变量很相似,都是生成一个对应的结构体实例,然后将变量存储在内部。

    我们看一下生成过程(代码经过简化):

    __attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {
        (void*)0,
        (__Block_byref_obj_0 *)&obj, 
        33554432, 
        sizeof(__Block_byref_obj_0), 
        __Block_byref_id_object_copy_131, 
        __Block_byref_id_object_dispose_131, 
        [[NSObject alloc] init]
    };
    

    其中,__Block_byref_obj_0 的结构如下所示:

    struct __Block_byref_obj_0 {
        void *__isa;
        // 指向自身实例的指针
        __Block_byref_obj_0 *__forwarding;
        int __flags;
        int __size;
        
        // obj
        void (*__Block_byref_id_object_copy)(void*, void*);
        
        // obj释放函数
        void (*__Block_byref_id_object_dispose)(void*);
        
        // 真正的对象
        id obj;
    };
    

    可以看到,__block 修饰的对象类型结构体,不仅包含与基本类型一样的成员,额外还包含了两个内存管理函数,用于在自身实例因Block的内存变化导致的变化时,包含的obj进行的保留和释放操作(Block的内存管理 -> __Block_byref_obj_0的内存变化 -> obj的内存变化)。

    这里,我们看一下这一对内存管理函数的简单实现:

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

    可以看到,以copy方法为例,实际上与描述信息 __main_block_desc_0_DATA 中的 __main_block_copy_0 函数实现一样,都是调用了 _Block_object_assign 函数。只不过参数有些许不同:

    src+40偏移量即为 __Block_byref_obj_0 结构体中的obj的地址。131即 BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT

    _Block_object_assign 实现为例(节选自苹果的Blocks源代码 Blocks/Sources/runtime.c):

    void _Block_object_assign(void *destAddr, const void *object, const int flags) {
        ...
        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
             __block id object;
             __block void (^object)(void);
             [^{ object; } copy];
             ********/
    
            // under manual retain release __block object/block variables are dangling
            _Block_assign((void *)object, destAddr);
            break;
            
            ...
        }
    }
    
    static void (*_Block_assign)(void *value, void **destptr) = _Block_assign_default;
    
    static void _Block_assign_default(void *value, void **destptr) {
        *destptr = value;
    }
    

    可以看到,在这种情况下,copy操作只是使用一个新的指针指向原始obj。

    在ARC下,实际上就是对obj进行了强引用,也就是retain操作;但是在非ARC下,这只是一个指针指向,可能造成悬垂指针访问,切记。

    而在 __main_block_desc_0_DATA 中,使用的copy和dispose函数与 __block 修饰的基本类型变量一致:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    

    最后,再看一下我们在Block函数体中对捕获变量的修改(代码已简化):

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        // 通过引用访问
        __Block_byref_obj_0 *obj = __cself->obj; 
        
        // 通过__forwarding指针访问到的永远是相同的obj
        (obj->__forwarding->obj) = [[NSMutableArray alloc] init];
    }
    

    3. 总结

    1. 在ARC环境下,Block在不捕获变量时,是NSGlobalBlock类型;否则,都是NSMallocBlock类型。
    2. 捕获到的基本数据类型变量或OC对象,直接存储值到Block的数据结构中,为值传递,外部修改无效。
    3. 捕获到的__block修饰的基本类型变量或OC对象,是以包装成的新的结构体实例的方式存储到Block的数据结构中,为引用传递,可以进行修改。
    4. ARC环境下,Block从栈上到被copy到堆上时,捕获的OC对象或是__block的OC对象,都会被retain;捕获的__block的基本类型变量,会创建一个新的结构体,保存在copy后的Block中。
    5. 非ARC环境下,使用__block修饰的OC对象,在被Block捕获后,可以防止循环引用(只是指针指向,没有retain操作,ARC下才是默认retain)。在ARC下,使用__weak修饰变量替代。

    相关文章

      网友评论

          本文标题:Block复习

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