理清 Block 底层结构及其捕获行为

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

内容简介:一个简单示例:将以上 Objective-C 源码转换成 c++ 相关源码,使用命令行 :c++ 的结构体与一般的类相似。
理清 Block 底层结构及其捕获行为

Block 的本质

本质

  1. Block 的本质是一个 Objective-C 对象,它内部也拥有一个 isa 指针。
  2. Block 是封装了函数及其调用环境的 Objective-C 对象

底层数据结构

一个简单示例:

int main(int argc, const char * argv[]) {

    void (^block)(void) = ^{
        NSLog(@"hey");
    };
    block();
    return 0;
}

复制代码

将以上 Objective-C 源码转换成 c++ 相关源码,使用命令行 : xcrun -sdk iphoneos xclang -arch arm64 -rewrite-objc 文件名

c++ 的结构体与一般的类相似。

int main(int argc, const char * argv[]) {

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

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

    return 0;
}
复制代码

其中 Block 的数据结构为:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};
复制代码

impl 变量数据结构:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr; 
};
复制代码

FuncPtr:函数实际调用的地址,因为 Block 可看作是捕获自动变量的匿名函数。

Desc 变量数据结构:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}
复制代码

Block 的类型

Objective-C 中 Block 有三种类型,其最终类型都是 NSBlock 。

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

Block 类型的不同,主要根据捕获变量的不同行为产生:

Block 类型 行为
NSGlobalBlock 没有访问 auto 变量
NSStackBlock 访问 auto 变量
NSMallocBlock NSStackBlock 调用 copy

在内存中的存储位置

理清 Block 底层结构及其捕获行为

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

copy 行为

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

Block 类型 副本源的配置存储域 复制效果
__NSConcreteStackBlock 从栈复制到堆
__NSConcreteGlobalBlock 数据段(常量区) 什么也不做
__NSConcreteMallocBlock 引用计数增加
  • 在 ARC 环境下,编译器会在以下情况自动将栈上的 Block 复制到堆上:
  1. Block 作为函数返回值
  2. 将 Block 赋值给 __strong 指针
  3. 苹果 Cocoa、GCD 等 api 中方法参数是 block 类型

在 ARC 环境下,声明的 block 属性用 copy 或 strong 修饰的效果是一样的,但在 MRC 环境下,则用 copy 修饰。

捕获变量

为了保证在 Block 内部能够正常访问外部变量,Block 有一套变量捕获机制:

变量类型 是否捕获到 Block 内部 访问方式
局部 auto 变量 值传递
局部 static 变量 指针传递
全局变量 直接访问

若局部 static 变量是基础类型 int val ,则访问方式为 int *val 若局部 static 变量是对象类型 JAObject *obj ,则访问方式为 JAObject **obj

基础类型变量

一个简单示例:

int age = 10;
// static int age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", age);
};
block();
复制代码
  • 捕获局部 auto 基础类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  int age; // 传递值
}
复制代码
  • 捕获局部 static 基础类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  int *age; // 传递指针
}
复制代码
  • 捕获全局基础类型变量生成的结构体 struct __main_block_impl_0 没有包含 age ,因为作用域为全局,可直接访问。

对象类型变量

一个简单示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();
复制代码
  • 捕获局部 auto 对象类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  JAPerson *person;
}
复制代码
  • 捕获局部 static 对象类型变量生成的 Block 结构体 struct __main_block_impl_0 变为:
struct __main_block_impl_0 {
  ···
  JAPerson **person;
}
复制代码
  • 捕获全局对象类型变量生成的结构体 struct __main_block_impl_0 没有包含 person ,因为作用域为全局,可直接访问。

copy 和 dispose 函数

当捕获的变量是对象类型或者使用 __Block 将变量包装成一个 __Block_byref_变量名_0 类型的 Objective-C 对象时,会产生 copydispose 函数。

一个简单示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();
复制代码

其中生成的 Block 的数据结构中多了 JAPerson 类型指针变量 person :

struct __main_block_impl_0 {
  ···
  JAPerson *person;
}
复制代码

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 和 dispose 底层相关源码

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);


// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
复制代码

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

  • 如果 Block 是在栈上,将不会对 auto 变量产生强引用。
  • 如果 Block 被拷贝到堆上,会调用 Block 内部的 copy 函数, copy 函数内部会调用 _Block_object_assign 函数, _Block_object_assign 函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretain)作出相应的内存管理操作。

注意:若此时变量类型为对象类型,这里仅限于 ARC 时会 retain ,MRC 时不会 retain 。

  • 如果 Block 从堆上移除,会调用 Block 内部的 dispose 函数, dispose 函数内部会调用 _Block_object_dispose 函数, _Block_object_dispose 函数会自动 release 引用的 auto 变量。

使用 __weak 修饰的 OC 代码转换对应的 c++ 代码会报错: error: cannot create __weak reference because the current deployment target does not support weak references 此时终端命令需支持 ARC 并指定 Runtime 版本: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

内存管理

修改局部 auto 变量

局部 static 变量(指针访问)、全局变量(直接访问)都可以在 Block 内部直接修改捕获的变量,而局部 auto 变量则主要通过使用 __block 存储域修饰符来修改捕获的变量。

  • __block 修饰符可以用于解决 Block 内部无法修改局部 auto 变量值的问题
  • __block 修饰符不能用于修饰全局变量、静态变量(static)

编译器会将 __block 修饰的变量包装成一个 Objective-C 对象。

一个简单示例:

__block int age = 10;
void (^block)(void) = ^{
   NSLog(@"age is %d", age);
};
block();
复制代码

其中 Block 的数据结构多了一个 __Block_byref_age_0 类型的指针:

struct __main_block_impl_0 {
  ···
  __Block_byref_age_0 *age; // by ref
}
复制代码

__Block_byref_age_0 结构体:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age; // age 真正存储的地方
};
复制代码

两个注意点:

    1. 此处指针 val 是指向 age 的指针,而第二个 val 指的是 age 的值。
理清 Block 底层结构及其捕获行为
    1. 源码里面通过 age->__forwarding->age 的方式去取值,是因为这两个 age 都可能仍在栈上,此时直接 age->age 访问会有问题,而 copy 操作时 __forwarding 会指向堆上的 __Block_byref_age_0 ,此时就算第一个 age 仍在栈上,通过 age->__forwarding 会重新指向堆上的 __Block_byref_age_0 ,此时再访问 age 便不会有问题 age->__forwarding->age
理清 Block 底层结构及其捕获行为
理清 Block 底层结构及其捕获行为

__block 的内存管理

使用 __block 修饰符时的内存管理情况:

  • 当 Block 存储在栈上时,并不会对 __block 变量强引用。
  • 当 Block 被 copy 到堆上时,会调用 Block 内部的 copy 函数, copy 函数会调用 __main_block_copy_0 函数对 __block 变量产生一个强引用。如下图
理清 Block 底层结构及其捕获行为
理清 Block 底层结构及其捕获行为
  • 当 Block 从堆上被移除时,会调用 Block 内部的 dispose 函数, dispose 函数会调用 _Block_object_dispose 函数自动 release __block 变量。如下图
理清 Block 底层结构及其捕获行为
理清 Block 底层结构及其捕获行为

__weak 和 __block 修饰时的引用情况

    1. 仅用 __weak 修饰

一个简单的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
    NSLog(@"person‘s age is %d", weakPerson.age);
};
复制代码
理清 Block 底层结构及其捕获行为
    1. 使用 __block __weak 修饰

一个简单的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__block __weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
     NSLog(@"person‘s age is %d", weakPerson.age);
};
block();
return 0;
复制代码
理清 Block 底层结构及其捕获行为

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

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

Reality Is Broken

Reality Is Broken

Jane McGonigal / Penguin Press HC, The / 2011-1-20 / USD 26.95

Visionary game designer Jane McGonigal reveals how we can harness the power of games to solve real-world problems and boost global happiness. More than 174 million Americans are gamers, and......一起来看看 《Reality Is Broken》 这本书的介绍吧!

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

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具