深入理解 Block

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

内容简介:下面我将通过一个简单的例子,结合源代码进行介绍使用由上面的源码,我们能猜想到:
  • 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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

Agile Web Development with Rails 4

Agile Web Development with Rails 4

Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2013-10-11 / USD 43.95

Ruby on Rails helps you produce high-quality, beautiful-looking web applications quickly. You concentrate on creating the application, and Rails takes care of the details. Tens of thousands of deve......一起来看看 《Agile Web Development with Rails 4》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具