理清 Block 底层结构及其捕获行为
栏目: Objective-C · 发布时间: 6年前
内容简介:一个简单示例:将以上 Objective-C 源码转换成 c++ 相关源码,使用命令行 :c++ 的结构体与一般的类相似。
Block 的本质
本质
- Block 的本质是一个 Objective-C 对象,它内部也拥有一个 isa 指针。
- 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 |
在内存中的存储位置
内存五大区:栈、堆、静态区(BSS 段)、常量区(数据段)、代码段
copy 行为
不同类型的 Block 调用 copy 操作,也会产生不同的复制效果:
Block 类型 | 副本源的配置存储域 | 复制效果 |
---|---|---|
__NSConcreteStackBlock | 栈 | 从栈复制到堆 |
__NSConcreteGlobalBlock | 数据段(常量区) | 什么也不做 |
__NSConcreteMallocBlock | 堆 | 引用计数增加 |
- 在 ARC 环境下,编译器会在以下情况自动将栈上的 Block 复制到堆上:
- Block 作为函数返回值
- 将 Block 赋值给 __strong 指针
- 苹果 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 对象时,会产生 copy
和 dispose
函数。
一个简单示例:
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 真正存储的地方 }; 复制代码
两个注意点:
-
- 此处指针 val 是指向 age 的指针,而第二个 val 指的是 age 的值。
-
- 源码里面通过
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 被 copy 到堆上时,会调用 Block 内部的
copy
函数,copy
函数会调用__main_block_copy_0
函数对 __block 变量产生一个强引用。如下图
- 当 Block 从堆上被移除时,会调用 Block 内部的
dispose
函数,dispose
函数会调用_Block_object_dispose
函数自动release
__block 变量。如下图
__weak 和 __block 修饰时的引用情况
-
- 仅用 __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 __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; 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 理清楚Vue的结构
- PHP 理清 foreach 潜规则
- 理清代码提交记录:代码管理的 git
- 教你理清SpringBoot与SpringMVC的关系
- 理清脉络——产品开发前都在做什么?
- 一文理清集成学习知识点(Boosting&Bagging)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Android群英传
徐宜生 / 电子工业出版社 / 2015-9 / 69.00元
《Android群英传》对具有一定Android开发基础的读者,以通俗易懂的语言介绍了Android开发的进阶技巧。《Android群英传》共分为13章,讲解了Android体系与系统架构、Android开发工具新接触、Android控件架构与自定义控件详解、ListView使用技巧、Android Scroll分析、Android绘图机制与处理技巧、Android动画机制与使用技巧、Activi......一起来看看 《Android群英传》 这本书的介绍吧!