美文网首页
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复习

    1. Block的基本结构 将包含Block的代码通过clang转换为c++代码(只用了c++的扩展struct,...

  • 面试复习-Block

    本质 block本质是一个oc对象,内部有isa指针 block是封装了函数和函数调用环境的oc对象 block内...

  • iOS基础:block 内如何修改 block 外部变量

    block 原理已有很多优秀的博客介绍过了,这里是对 block 相关知识的复习巩固 在 block 内部修改其外...

  • iOS复习之Block

    iOS面试中如何优雅回答Block iOS block循环引用

  • C语言参数传递

    C语言参数传递 前言 最近复习Block相关知识,其中有个问题:block中为什么不能改变(这里值重新复制)被截获...

  • Ios复习--Block的使用

    Block 是一种特殊的数据类型,默认存储在栈中,若对Block 进行一次Copy 则Block会进入堆中 1.B...

  • 关于Block的几个知识点分析说明

    前言最近复习一下Block的知识,发现一些书籍和网上的文章对Block的某些知识点的解释存在不清晰甚至存在错误。所...

  • oracle中的buffer cache 详解

    1、block、buffer的概念 复习段区块 2、buffer cache的意义 减少IO 物理IO:磁盘读 逻...

  • 复习一下block 待续。。

    block ios 定义: void (^aBlock)(NSString*x,NSString*y); 函数体:...

  • iOS开发之Block原理探究

    Block概述 Block本质 Block调用 Block分类 Block循环引用 Block原理探究 Block...

网友评论

      本文标题:Block复习

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