iOS Block 部分一
主要讲解 Block 的底层实现原理;
Block部分一
Block部分二
Block部分三
Block知识点总结
基础知识 C++中结构体的构造函数是怎么实现的?
很多 OC
对象低层都转化为了C++
的结构体, 由于不懂 C++
的语法, 所以网上查了下一些C++
结构体的基础知识;
结构体的构造函数
typedef struct Test{
int id;
string name;
// 用以不初始化就构造结构体
Test(){} ;
//只初始化name
Test(string _name) {
name = _name;
}
/*
同时初始化id,name , 把入参的 id 和 name 为结构体中的 id 和 name 赋值;
参考下方 OC 类的构造函数
*/
Test(inr _id,string _name): id(_id),name(_name)}{};
};
类似于我们的构造函数实现;
///.h 文件声明
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
- (instancetype)initWithName:(NSString *)name
age:(NSInteger)age;
@end
NS_ASSUME_NONNULL_END
///.m文件实现
#import "Person.h"
@interface Person ()
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation Person
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
self = [super init];
if (self) {
self.name = name;
self.age = age;
}
return self;
}
@end
1. 什么是Block?
Block
是一个对象, 对象中封装了一个函数以及函数执行的上下文;Block
的调用本质是函数
的调用;
2. Block的底层实现是什么?
block 的底层是一个结构体__XXXX_block_impl_0
测试代码如下, 一个最简单的Block
; 通过指令转换为C++
文件后得到如下结果;
///viewDidLoad中写一个最简单的block
- (void)viewDidLoad {
[super viewDidLoad];
///case1 block的底层结构
^{
NSLog(@"Block内部内容;");
NSLog(@"Block内部内容;");
NSLog(@"Block内部内容;");
NSLog(@"Block内部内容;");
};
}
通过指令转化为C++文件, 底层实现为
static void _I_ViewController1_viewDidLoad(ViewController1 * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController1"))}, sel_registerName("viewDidLoad"));
/*
Block的底层实现如下; __XXXX_block_impl_0结构体即为block的底层结构
通过__XXXX_block_impl_0构造函数来初始化这个结构体, 入参两个参数
__XXXX_block_func_0: 实际block中需要执行的的代码块
__XXXXX_block_desc_0_DATA: block 的一些信息
*/
((void (*)())&__ViewController1__viewDidLoad_block_impl_0((void *)__ViewController1__viewDidLoad_block_func_0, &__ViewController1__viewDidLoad_block_desc_0_DATA));
}
===>
block 的底层为一个结构体__XXXX_block_impl_0;
///__XXXX_block_impl_0的结构如下
struct __ViewController1__viewDidLoad_block_impl_0 {
///bock 的实现信息
struct __block_impl impl;
///block 的描述信息
struct __ViewController1__viewDidLoad_block_desc_0* Desc;
__ViewController1__viewDidLoad_block_impl_0(void *fp, struct
/// block 的构造函数, 为结构体里面的变量赋值, 参考 OC 类的构造函数
__ViewController1__viewDidLoad_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
///block中需要执行的代码块, 具体放在内存中的代码段;
impl.FuncPtr = fp;
Desc = desc;
}
};
===>
///实际block中的代码块(这个 block 中就是那4个 NSLog)
static void __ViewController1__viewDidLoad_block_func_0(struct __ViewController1__viewDidLoad_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x9__266tpqd76sb1c4cm_mvpjz40000gn_T_ViewController1_b5ce37_mi_0);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x9__266tpqd76sb1c4cm_mvpjz40000gn_T_ViewController1_b5ce37_mi_1);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x9__266tpqd76sb1c4cm_mvpjz40000gn_T_ViewController1_b5ce37_mi_2);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x9__266tpqd76sb1c4cm_mvpjz40000gn_T_ViewController1_b5ce37_mi_3);
}
===>
///block 的实现信息
struct __block_impl {
///isa 指针
void *isa;
///标志; 通过构造函数可以看到传入的是0;
int Flags;
///保留字段
int Reserved;
/*
block 中需要执行的代码块封装的函数地址;
__XXXX_block_impl_0结构体的构造函数会为其赋值
*/
void *FuncPtr;
};
===>
///block的一些描述信息
static struct __ViewController1__viewDidLoad_block_desc_0 {
///保留字段
size_t reserved;
///block 所占的空间(看下面对sizeof(struct __main_block_impl_0)即可得知)
size_t Block_size;
} __ViewController1__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController1__viewDidLoad_block_impl_0)};
总结底层的几个结构体和函数
XXXX_block_impl_0
: 就是block
对象的底层结构体;
__block_impl
: 就是封装了block
具体实现的结构体;
XXXX_block_func_0
: 就是封装了block
内部执行代码块的函数;
XXXX_block_desc_0
: 就是封装了block
的一些基本信息;
注意: 如果访问了
auto
变量(ARC
), 因为对block
进行了copy
到堆区的操作, 所以通过clang
后会出现两套block
, 而调用部分会变成XXXX_block_impl_1
,XXXX_block_func_1
等, 因为这个是对XXXX_block_impl_0
,XXXX_block_func_1
的拷贝后的;
3. 如何定义Block并且通过名字调用的?
在弄清这个问题首先印证一个问题;一个结构体嵌套结构体时, 如果第一个变量是结构体, 则可以通过内存地址直接访问内层结构体中的变量;代码如下:
#import "ViewController2.h"
@interface ViewController2 ()
@end
///case2的印证结构体嵌套时使用
struct SubStruct {
int a;
};
struct SuperStruct {
struct SubStruct subs;
int b;
};
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
struct SuperStruct supers = {{10}, 2};
NSLog(@"%p %p", &(supers.subs), &supers);
///(强制转换为int值)(强制转换为SubStruct)&取supers的地址->访问SubStruct中变量
int subStruct_a = (int)((struct SubStruct *)&supers)->a;
NSLog(@"通过外层结构体地址强行访问内存结构体的变量 %d", subStruct_a);
}
两个地址是一样的; 由于substruct
是 superstruct
的第一个变量, 所以substruct
和外层的superstruct
地址是一样的;
2020-06-24 17:19:15.005827+0800 BlockMore1[6213:90176] 0x7ffee413cc88 0x7ffee413cc88
可以通过内存地址方式superstruct
调用内层substruct
变量;
2020-06-24 17:19:15.005918+0800 BlockMore1[6213:90176] 通过外层结构体地址强行访问内存结构体的变量 10
通过下面代码来探究block
的调用方式;
///case2 定义一个 block 然后调用
void(^Case2Block)(void) = ^{
NSLog(@"Block内部内容;");
NSLog(@"Block内部内容;");
NSLog(@"Block内部内容;");
NSLog(@"Block内部内容;");
};
Case2Block();
通过指令转换为C++
文件后的代码为
///Case2Block的定义过程
void(*Case2Block)(void) = ((void (*)())&__ViewController2__viewDidLoad_block_impl_0((void *)__ViewController2__viewDidLoad_block_func_0, &__ViewController2__viewDidLoad_block_desc_0_DATA));
///Case2Block的实际调用
((void (*)(__block_impl *))((__block_impl *)Case2Block)->FuncPtr)((__block_impl *)Case2Block);
直接看不够直观; 我们将一些强制转换的相关代码去掉;
- 定义部分
强制转换相关的代码部分去掉后,定义block
过程伪代码大致如下, 调用__XXXX_block_impl_0
的构造函数, 两个入参
__XXXX_block_func_0
: 实际block
中需要执行的代码块,
&__XXXX_block_desc_0_DATA
: 这个block
一些信息;
实际第三个参数flags
, 由于构造函数中直接赋值flags=0
(case1的讲解), 所以这里不用传入;
最后的结果就是讲生成的结构体的通过&取地址然后赋值给Case2Block
指针;
简化后为:void(*Case2Block) = &__XXXX_block_impl_0(__XXXX_block_func_0,&__XXXX_block_desc_0_DATA)
- 调用部分
强制转换相关的代码部分去掉后,调用block
过程伪代码大致如下
Case2Block
的地址就是__XXXX_block_impl_0
地址, 通过上面结构体嵌套的论证, 我们可以知道
这种方式最后调用的是__block_impl
结构体中的FuncPtr
(就是实际Case2Block
中的需要执行的代码块);
简化后为:Case2Block->FuncPtr(Case2Block)
4. 什么是 Block 的变量捕获(capture)?
首先看下方代码, 我们都知道最后的 打印结果为 a = 100, b = 200, c = 3, d = 400
; 原因是 a
是全局变量, b
是全局静态变量, c
是局部变量, d
是静态局部变量; 但是为什么会这样? 我们从源码角度分析下为什么;
#import "ViewController3.h"
@interface ViewController3 ()
@end
int a = 1;//全局变量, 整个工程内都有效;
static int b = 2;//静态全局变量, 只在定义它的文件内有效;
@implementation ViewController3
- (void)viewDidLoad {
[super viewDidLoad];
/******************************************************************/
///case3 block 的变量捕获
int c = 3;//局部变量, 只在定义他的函数内有效;1
static int d = 4;//静态局部变量, 只在定义他的函数内有效, 且内存只分配一次;
/*
block将局部变量捕获到block内部, 主要一个是捕获值, 一个是捕获址;全局变量不捕获;
*/
void(^Case3Block)(void) = ^{
NSLog(@"a = %d, b = %d, c = %d, d = %d", a, b, c, d);
};
a = 100;
b = 200;
c = 300;
d = 400;
Case3Block();
}
@end
2020-06-25 09:50:45.294219+0800 BlockMore1[2775:24440] a = 100, b = 200, c = 3, d = 400
将上方的代码通过指令编译为C++
文件后, 我们可以看到Case3Block
的相关代码如下
/*
注意: 全局变量a和b底层实现也是如此, 并没有发生变化;
*/
int a = 1;
static int b = 2;
/*
Case3Block的底层实现,已经变化, block内部重新定义两个变量, 一个全新的c变量, 和一个地址变量d;
*/
struct __ViewController3__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController3__viewDidLoad_block_desc_0* Desc;
int c;
int *d;
__ViewController3__viewDidLoad_block_impl_0(void *fp, struct __ViewController3__viewDidLoad_block_desc_0 *desc, int _c, int *_d, int flags=0) : c(_c), d(_d) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
看过上方源码后相信就可以很直观的得知:
全局变量, block
内部并不会捕获创建新的变量进行存储, 所以更改全局变量后,仍然是直接调用全局变量;
局部变量: block
会进行变量捕获, 局部变量是捕获值, 静态局部变量捕获址;
补充: self是全局变量还是局部变量?block内是否会捕获self?
self是
局部变量, block
内部会捕获self
;
通过底层代码验证捕获self
;
@interface ViewController31 () {
int _age;
}
@property (nonatomic, assign) int count;
@end
@implementation ViewController31
- (void)viewDidLoad {
[super viewDidLoad];
///不论是访问成员变量还是访问属性, 最终都是会将self捕获到self内部;
^ {
NSLog(@"age = %d, count = %d", _age, self.count);
};
static void _I_ViewController31_viewDidLoad(ViewController31 * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController31"))}, sel_registerName("viewDidLoad"));
///block的定义
((void (*)())&__ViewController31__viewDidLoad_block_impl_0((void *)__ViewController31__viewDidLoad_block_func_0, &__ViewController31__viewDidLoad_block_desc_0_DATA, self, 570425344));
}
///block的实现
struct __ViewController31__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController31__viewDidLoad_block_desc_0* Desc;
///新增变量*self, 用来存储self
ViewController31 *self;
__ViewController31__viewDidLoad_block_impl_0(void *fp, struct __ViewController31__viewDidLoad_block_desc_0 *desc, ViewController31 *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
通过底层代码验证self
是局部变量;
- (void)test31 {
NSLog(@"实例方法self指向当前实例对象");
}
+ (void)test32 {
NSLog(@"类方法self指向当前类对象");
}
所有的实例方法中底层默认传入*self
指向此实例对象; 所以可以得知self
是局部变量; 类方法中调用self
是指向当前类对象;但是无论是实例方法还是类方法转换为C++
文件可以看到都是有入参self
参数的;
/*
每个实例方法的底层, 是默认会入参两个参数
*self 指向当前的实例对象
_cmd 指向当前的方法
*/
static void _I_ViewController31_test31(ViewController31 * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x9__266tpqd76sb1c4cm_mvpjz40000gn_T_ViewController31_ab9aa9_mi_1);
}
/*
每个类方法的底层, 是默认会入参两个参数
self 指向当前的类对象
_cmd 指向当前的方法
*/
static void _C_ViewController31_test32(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x9__266tpqd76sb1c4cm_mvpjz40000gn_T_ViewController31_ab9aa9_mi_2);
}
通过指令将OC文件转换为C++文件
指令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 文件.m -o 文件-arm64.cpp
如果需要链接其他框架, 使用-framework
参数; 例:-framework UIKit
参考文章和下载链接
测试代码
iOS clang指令报错问题总结
Apple 一些源码的下载地址
C++中结构体的构造函数
全局变量、静态全局变量、静态局部变量和普通局部变量的区别