深入理解 Block

栏目: Objective-C · 发布时间: 5年前

内容简介:下面我将通过一个简单的例子,结合源代码进行介绍使用由上面的源码,我们能猜想到:
  • Block 是 C 语言的扩充功能
  • Block 是带有自动变量(局部变量)的匿名函数

本质

  • Block 是一个 Objc 对象

底层实现

下面我将通过一个简单的例子,结合源代码进行介绍

int main(int argc, const char * argv[]) {
    void (^blk)(void) = ^{ printf("Hello Block\n"); };
    blk();
    return 0;
}
复制代码

使用 clang -rewrite-objc main.m ,我们可以将 Objc 的源码转成 Cpp 的相关源码:

int main(int argc, const char * argv[]) {
    // Block 的创建
    void (*blk)(void) =
        (void (*)(void))&__main_block_impl_0(
            (void *)__main_block_func_0, &__main_block_desc_0_DATA);
    
    // Block 的使用
    ((void (*)(struct __block_impl *))(
        (struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
    return 0;
}
复制代码

由上面的源码,我们能猜想到:

__main_block_impl_0
FuncPtr

从这里为切入点看看上面提到的都是啥

Block 的数据结构

Block 的真身:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    // 省略了构造函数
};
复制代码
  • Block 其实不是一个匿名函数,他是一个结构体
  • __main_block_impl_0 名字的命名规则: __所在函数_block_impl_序号

impl 变量的数据结构

__main_block_impl_0 的主要数据:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
复制代码
  • isa 指针: 体现了 Block 是 Objc 对象的本质
  • FuncPtr 指针: 其实就是一个函数指针,指向所谓的匿名函数。

Desc 变量的数据结构

__main_block_desc_0 中放着 Block 的描述信息

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};
复制代码

"匿名函数"

__main_block_impl_0 即 Block 创建时候使用到了 __main_block_func_0 正是下面的函数:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Hello Block\n");
}
复制代码
  • 这部分和 ^{ printf("Hello Block\n"); } 十分相似,由此可看出: 通过 Blocks 使用的匿名函数实际上被作为简单的 C 语言函数来处理
  • 函数名是根据 Block 语法所属的函数名(此处 main )和该 Block 语法在函数出现的顺序值(此处为 0)来命名的。
  • 函数的参数 __cself 相当于 C++ 实例方法中指向实例自身的变量 this ,或是 Objective-C 实例方法中指向对象自身的变量 self ,即参数 __cself 为指向 Block 的变量。
  • 上面的 (*blk->impl.FuncPtr)(blk); 中的 blk 就是 __cself

介绍了基本的数据结构,下面到回到一开始的 main 函数,看看 Block 具体的使用

Block 的创建

void (*blk)(void) =
        (void (*)(void))&__main_block_impl_0(
            (void *)__main_block_func_0, &__main_block_desc_0_DATA);
/** 去掉转换的部分
 struct __main_block_impl_0 tmp =
     __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
 struct __main_block_impl_0 *blk = &tmp;
*/
复制代码
  • void (^blk)(void) 就是是一个 struct __main_block_impl_0 *blk
  • Block 表达式的其实就是通过 所谓的匿名函数 __main_block_func_0 的函数指针 创建一个 __main_block_impl_0 结构体,我们用的时候是拿到了这个结构体的指针。

Block 的使用

((void (*)(struct __block_impl *))(
        (struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
/** 去掉转换的部分
 (*blk->impl.FuncPtr)(blk);
*/
复制代码
  • Block 真正的使用方法就是使用 __main_block_impl_0 中的函数指针 FuncPtr
  • (blk) 这里是传入自己,就是给 _cself 传参

Block 的类型

从 Block 中的简单实现中,我们从 isa 中发现 Block 的本质是 Objc 对象,是对象就有不同类型的类。因此,Block 当然有不同的类型

在 Apple 的 libclosure-73 中的 data.c 上可见, isa 可指向:

void * _NSConcreteStackBlock[32] = { 0 }; // 栈上创建的block
void * _NSConcreteMallocBlock[32] = { 0 }; // 堆上创建的block
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 }; // 作为全局变量的block
void * _NSConcreteWeakBlockVariable[32] = { 0 };
复制代码

其中我们最常见的是:

Block的类型 名称 行为 存储位置
_NSConcreteStackBlock 栈Block 捕获了局部变量
_NSConcreteMallocBlock 堆Block 对栈Block调用copy所得
_NSConcreteGlobalBlock 全局Block 定义在全局变量中 常量区(数据段)

PS:内存五大区:栈、堆、静态区(BSS 段)、常量区(数据段)、代码段

关于 copy 操作

对象有 copy 操作,Block 也有 copy 操作。不同类型的 Block 调用 copy 操作,也会产生不同的复制效果:

Block的类型 副本源的配置存储域 复制效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 常量区(数据段) 什么也不做
_NSConcreteMallocBlock 引用计数增加

栈上的 Block 复制到堆上的时机

  • 调用 Block 的 copy 实例方法

编译器自动调用 _Block_copy 函数情况

id

PS:在 ARC 环境下,声明的 Block 属性用 copystrong 修饰的效果是一样的,但在 MRC 环境下用 copy 修饰。

捕获变量

基础类型变量

以全局变量、静态全局变量、局部变量、静态局部变量为例:

int global_val = 1;
static int static_global_val = 2;

int main(int argc, const char * argv[]) {
    int val = 3;
    static int static_val = 4;
    
    void (^blk)(void) = ^{
        printf("global_val is %d\n", global_val);
        printf("static_global_val is %d\n", static_global_val);
        printf("val is %d\n", val);
        printf("static_val is %d\n", static_val);
    };
    
    blk();
    
    return 0;
}
复制代码

转换后“匿名函数”对应的代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int val = __cself->val; // bound by copy
    int *static_val = __cself->static_val; // bound by copy

    printf("global_val is %d\n", global_val);
    printf("static_global_val is %d\n", static_global_val);
    printf("val is %d\n", val);
    printf("static_val is %d\n", (*static_val));
}
复制代码
  • 全局变量、静态全局变量 : 作用域为全局,因此在 Block 中是直接访问的。
  • 局部变量 : 生成的 __main_block_impl_0 中存在 val 实例,因此对于局部变量,Block 只是单纯的复制创建时候 局部变量的瞬时值 ,我们可以使用值,但不能修改值。
struct __main_block_impl_0 {
  // ...
  int val; // 值传递
  // ...
};
复制代码
  • 静态局部变量 : 生成的 __main_block_impl_0 中存在 static_val 指针,因此 Block 是在创建的时候获取 静态局部变量的指针值
struct __main_block_impl_0 {
    // ...
    int *static_val; // 指针传递
    // ...
};
复制代码

对象类型变量

模仿基础类型变量,实例化四个不一样的 SCPeople 变量:

int main(int argc, const char * argv[]) {
    // 省略初始化
    [globalPeople introduce];
    [staticGlobalPeople introduce];
    [people introduce];
    [staticPeople introduce];
    
    return 0;
}
复制代码

转换后"匿名函数"对应的代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    SCPeople *people = __cself->people; // bound by copy
    SCPeople **staticPeople = __cself->staticPeople; // bound by copy

    // 省略 objc_msgSend 转换
    [globalPeople introduce];
    [staticGlobalPeople introduce];
    [people introduce];
    [*staticPeople introduce];
}
复制代码
  • 全局对象、静态全局对象 : 作用域依然是全局,因此在 Block 中是直接访问的。
  • 局部对象 : 生成的 __main_block_impl_0 中存在 people 指针实例,因此 Block 获取的是 指针瞬间值 ,我们可以在 Block 中通过指针可以操作对象,但是不能改变指针的值。
struct __main_block_impl_0 {
    // ...
    SCPeople *people;
    // ...
};
复制代码
  • 静态局部对象 : 生成的 __main_block_impl_0 中存在 staticPeople 指针的指针,因此 Block 是在创建的时候获取 静态局部对象的指针值 (即指针的指针)。
struct __main_block_impl_0 {
    // ...
    SCPeople **staticPeople;
    // ...
};
复制代码

小结

通过对基础类型、对象类型与四种不同的变量进行排列组合的小 Demo,不难得出下面的规则:

变量类型 是否捕获到 Block 内部 访问方式
全局变量 直接访问
静态全局变量 直接访问
局部变量 值访问
静态局部变量 指针访问

PS:

  • 基础类型和对象指针类型其实是一样的,只不过指针的指针看起来比较绕而已。
  • 全局变量与静态全局变量的存储方式、生命周期是相同的。但是作用域不同,全局变量在所有文件中都可以访问到,而静态全局变量只能在其申明的文件中才能访问到。

变量修改

上面的篇幅通过底层实现,向大家介绍了 Block 这个所谓"匿名函数"是如何捕获变量的,但是一些时候我们需要修改 Block 中捕获的变量:

修改全局变量或静态全局变量

全局变量与静态全局变量的作用域都是全局的,自然在 Block 内外的变量操作都是一样的。

修改静态局部变量

在上面变量捕获的章节中,我们得知 Block 捕获的是静态局部变量的指针值,因此我们可以在 Block 内部改变静态局部变量的值(底层是通过指针来进行操作的)。

修改局部变量

使用 __block 修饰符来指定我们想改变的局部变量,达到在 Block 中修改的需要。

我们用同样的方式,通过底层实现认识一下 __block ,举一个:chestnut::

__block int val = 0;
void (^blk)(void) = ^{ val = 1; };
blk();
复制代码

经过转换的代码中出现了和单纯捕获局部变量不同的代码:

__Block_byref_val_0 结构体

struct __Block_byref_val_0 {
    void *__isa; // 一个 Objc 对象的体现
    __Block_byref_val_0 *__forwarding; // 指向该实例自身的指针
    int __flags;
    int __size;
    int val; // 原局部变量
};
复制代码
  • 编译器会将 __block 修饰的变量包装成一个 Objc 对象。

val 转换成 __Block_byref_val_0

__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
    (void*)0,
    (__Block_byref_val_0 *)&val,
    0,
    sizeof(__Block_byref_val_0),
    0
};
复制代码

__main_block_impl_0 捕获的变量

struct __main_block_impl_0 {
    // ...
    __Block_byref_val_0 *val; // by ref
    // ...
};
复制代码
  • Block的 __main_block_impl_0 结构体实例持有指向 __block 变量的 __Block_byref_val_0 结构体实例的指针。
  • 这个捕获方式和捕获静态局部变量相似,都是指针传递

"匿名函数"的操作

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_val_0 *val = __cself->val; // bound by ref
    
    (val->__forwarding->val) = 1;
}
复制代码

(val->__forwarding->val) 解释

  • 左边的 val__main_block_impl_0 中的 val ,这个 val 通过 __block int val 的地址初始化
  • 右边的 val__Block_byref_val_0 中的 val ,正是 __block int valval
  • __forwarding 在这里只是单纯指向了自己而已
    深入理解 Block

__forwarding 的存在意义

上面的"栈Blcok"中 __forwarding 在这里只是单纯指向自己,但是在当"栈Blcok"复制变成"堆Block"后, __forwarding 就有他的存在意义了:

深入理解 Block
PS: __block

修饰符不能用于修饰全局变量、静态变量。

内存管理

Block 与对象类型

copy & dispose

众所周知,对象其实也是使用一个指针指向对象的存储空间,我们的对象值其实也是指针值。虽然是看似对象类型的捕获与基础类型的指针类型捕获差不多,但是捕获对象的转换代码比基础指针类型的转换代码要多。 ( __block 变量也会变成一个对象,因此下面的内容也适用于 __block 修饰局部变量的情况) 。多出来的部分是与内存管理相关的 copy 函数与 dispose 函数:

底层实现

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->people, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_dispose((void*)src->staticPeople, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
复制代码

这两个函数在 Block 数据结构存在于 Desc 变量中:

static struct __main_block_desc_0 {
  // ...
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
}; // 省略了初始化好的结构体
复制代码

函数调用时机

函数 调用时机
copy 函数 栈上的 Block 复制到堆时
dispose 函数 堆上的 Block 被废弃时

函数意义

  • copy 函数中的 _Block_object_assign 函数相当于内存管理中的 retain 函数,将对象赋值在对象类型的结构体成员变量中。
  • dispose 函数中的 _Block_object_dispose 函数相当于内存管理中的 release 函数,释放赋值在对象类型的结构体变量中的对象。
  • 通过 copydispose 并配合 Objc 运行时库对其的调用可以实现内存管理

※ 例子

当 Block 内部访问了对象类型的局部变量时:

  • 当 Block 存储在栈上时 : Block 不会对局部变量产生强引用。
  • 当 Block 被 copy 到堆上时 : Block 会调用内部的 copy 函数, copy 函数内部会调用 _Block_object_assign 函数, _Block_object_assign 函数会根据局部变量的修饰符( __strong__weak__unsafe_unretained )作出相应的内存管理操作。(注意: 多个 Block 对同一个对象进行强引用的时,堆上只会存在一个该对象)
  • 当 Block 从堆上被移除时 : Block 会调用内部的 dispose 函数, dispose 函数内部会调用 _Block_object_dispose 函数, _Block_object_dispose 函数会自动 release 引用的局部变量。(注意: 直到被引用的对象的引用计数为 0,这个堆上的该对象才会真正释放)

PS:对于 __block 变量,Block 永远都是对 __Block_byref_局部变量名_0 进行强引用。如果 __block 修饰符背后还有其他修饰符,那么这些修饰符是用于修饰 __Block_byref_局部变量名_0 中的 局部变量 的。

现象:Block 中使用的赋值给附有 __strong 修饰符的局部变量的对象和复制到堆上的 __block 变量由于被堆的 Block 所持有,因而可超出其变量作用域而存在。

循环引用

由于 Block 内部能强引用捕获的对象,因此当该 Block 被对象强引用的时候就是注意以下的引用循环问题了:

深入理解 Block

ARC 环境下解决方案

  1. 弱引用持有:使用 __weak__unsafe_unretained 捕获对象解决

    深入理解 Block
    • weak 修饰的指针变量,在指向的内存地址销毁后,会在 Runtime 的机制下,自动置为 nil
    • _unsafe_unretained 不会置为 nil ,容易出现悬垂指针,发生崩溃。但是 _unsafe_unretained__weak 效率高。
  2. 使用 __block 变量 :使用 __block 修饰对象,在 block 内部用完该对象后,将 __block 变量置为 nil 即可。虽然能控制对象的持有期间,并且能将其他对象赋值在 __block 变量中,但是必须执行该 block。(意味着这个对象的生命周期完全归我们控制)

    深入理解 Block

MRC 环境下解决方案

  1. 弱引用持有:使用 __unsafe_unretained 捕获对象
  2. 直接使用 __block 修饰对象,无需手动将对象置为 nil ,因为底层 _Block_object_assign 函数在 MRC 环境下对 block 内部的对象不会进行 retain 操作。

MRC 下的 Block

ARC 无效时,需要手动将 Block 从栈复制到堆,也需要手动释放 Block

  • 对于栈上的 Block 调用 retain 实例方法是不起作用的
  • 对于栈上的 Block 需要调用一次 copy 实例方式(引用计数+1),将其配置在堆上,才可继续使用 retain 实例方法
  • 需要减少引用的时候,只需调用 release 实例方法即可。
  • 对于在 C 语言中使用 Block,需要使用 Block_copyBlock_release 代替 copyrelease

以上所述就是小编给大家介绍的《深入理解 Block》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C和C++代码精粹

C和C++代码精粹

阿林森 / 董慧颖 / 人民邮电出版社 / 2003-4-1 / 59.00

《C和C++代码精粹》基于作者备受好评的C/C++ User Journal杂志上的每月专栏,通过大量完全符合ISO标准C++的程序集合,说明了C++真正强大的威力,是C和C++职业程序员的实践指南。可以帮助有一定经验的C和C++程序员深入学习这两种密切相关的语言,对书中代码的参悟和应用,可以帮助他们从根本上提高使用程序的效率。一起来看看 《C和C++代码精粹》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码