iOS探索篇 | 第二部分:深入了解Block使用方法及原理
2024-09-24 12:04:19发布 浏览132次 信息编号:166242
平台友情提醒:凡是以各种理由向你收取费用,均有骗子嫌疑,请提高警惕,不要轻易支付。
iOS探索篇 | 第二部分:深入了解Block使用方法及原理
探索系列已发表文章列表,感兴趣的同学可以阅读:
------- 文本开始 -------
介绍
在日常的 iOS 开发中, 的使用频率还是比较高的,我们并不是每天都会做启动优化、性能优化,但是我们可能每天都会用到 。本文就来给大家讲讲在日常开发中, 中值得我们关注的技术点,一起来学习吧。
编码标准
// 定义一个 Block
typedef returnType (^BlockName)(parameterA, parameterB, ...);
eg: typedef void (^RequestResult)(BOOL result);
// 实例
^{
NSLog(@"This is a block");
}
自然
Block 本质上是一个 C 对象,其内部也有一个 isa 指针。它是一个封装了函数和函数调用环境的 C 对象,可以添加到 和 等集合中。它基于 C 语言和运行时特性,与标准 C 函数有些相似。但除了可执行代码之外,它还包括将变量自动绑定到堆或栈。
常见介绍
void (^exampleBlock)(void) = ^{
// block
};
NSLog(@"exampleBlock is: %@",[exampleBlock class]);
打印日志:是:
如果一个块不访问外部的局部变量,或者访问全局变量或者静态局部变量,则该块为全局块,数据保存在全局区域。
int temp = 100;
void (^exampleBlock)(void) = ^{
// block
NSLog(@"exampleBlock is: %d", temp);
};
NSLog(@"exampleBlock is: %@",[exampleBlock class]);
打印日志:is:???这不是我们约定好的吗?为什么会打印出来呢?这是因为我们用了ARC,Xcode默认帮我们做了很多事情。
我们可以进入Build,找到-C,并将其设置为No,然后再次运行代码。你会看到打印的日志是:
如果某个 block 访问了外部的局部变量,那么该 block 就是一个堆栈 block,并且存储在堆栈区域。由于堆栈区域的释放由系统控制,作用域结束后,堆栈中代码的内存就会被破坏。如果此时再次调用该 block,就会出现问题。(注意:此代码在 MRC 下运行)例如:
void (^simpleBlock)(void);
void callFunc() {
int age = 10;
simpleBlock = ^{
NSLog(@"simpleBlock-----%d", age);
};
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
callFunc();
simpleBlock();
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return 0;
}
打印日志:--------
当复制一个类型的 Block 时,会把这个 Block 从栈复制到堆上,堆上的 Block 类型就是这个类型。在 ARC 环境下,编译器会根据情况自动把 Block 从栈复制到堆上。具体有四种情况会进行复制:
简单来说就是允许块访问和修改外部变量。在ARC环境下,还可以用它来防止循环引用。
__block int age = 10;
void (^exampleBlock)(void) = ^{
// block
NSLog(@"1.age is: %d", age);
age = 16;
NSLog(@"2.age is: %d", age);
};
exampleBlock();
NSLog(@"3.age is: %d", age);
主要用于解决自动变量的值在块内部不能被修改的问题,为什么加了修饰符之后自动变量的值就可以被修改了呢?
这是因为,加上修饰之后,编译器会把该变量打包成一个结构体,结构体里面的*是指向自身的指针,外部的auto变量也是保存在结构体内部的。
struct __Block_byref_val_0 {
void *__isa; // isa指针
__Block_byref_val_0 *__forwarding;
int __flags;
int __size; // Block结构体大小
int age; // 捕获到的变量
}
从上图可以看出,如果block在栈上,那么指针指向的是自己,当block从栈复制到堆上时,栈上的指针指向复制到堆上的结构。堆上的结构依然指向自己,也就是age->得到的是堆上的结构,age->->age会把堆上的age的值赋值为16。所以,不管是栈上的结构还是堆上的结构,最终使用的都是堆上的结构里的数据。
简单来说就是防止循环引用。
self本身会对block有强引用,而block也会对self有强引用,这样就会造成循环引用问题。我们可以利用这个方法来打破循环,让block对象对self有弱引用。
此时要注意,由于 block 对 self 的引用是弱引用,所以有可能在 block 执行时,self 对象本身已经被释放了。那么如何保证在 block 内部不释放 self 对象呢?这就导致了下面的效果。
__weak __typeof(self) weakSelf = self;
void (^exampleBlock)(void) = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf exampleFunc];
};
这确保了在块范围结束之前有一个可在块内使用的对象。
不过即使如此,还是存在一种场景,那就是在执行()=;之前,对象已经被释放了。此时如果给self对象发送消息,是没有问题的。-C的消息发送机制允许我们向nil对象发送消息,不会出现任何问题。
但是如果有一些额外的操作,比如将self添加到数组中,程序就会崩溃,因为self为nil。
我们可以添加一层安全性来解决这个问题,比如:
__weak __typeof(self) weakSelf = self;
void (^exampleBlock)(void) = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
// Add operation here
}
};
扩展你的知识
修改Block中的外部、和对象时,是否需要添加修改?
NSMutableArray *mutableArray = [[NSMutableArray alloc] init];
[mutableArray addObject:@"1"];
void (^exampleBlock)(void) = ^{
// block
[mutableArray addObject:@"2"];
};
exampleBlock();
NSLog(@"mutableArray: %@", mutableArray);
打印日志:
:(1,2)
答案是:不需要。因为在块内部,我们只是使用对象的内存地址来向其中添加内容,并没有修改它的内存地址,所以不用它也能正确执行。而当我们只使用局部变量的内存地址而不修改它的内存地址时,是不需要添加的,如果添加的话系统会自动创建相应的结构体,这样就比较冗余,效率也比较低。
Block内部数据结构如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
结构体成员含义如下:
isa:指向类的指针,即块的类型
flags:按位表示该块的一些附加信息,比如确定块类型、确定块引用计数、确定块是否需要执行辅助功能等。
:保留变量;
:block函数指针,指向具体block实现的函数调用地址。block内部的执行代码就在这个函数里;
:结构,块的附加描述信息,包括副本/功能、块大小、以及保留变量;
:因为块有闭包,所以可以访问块外的局部变量,这些是外部局部变量或者复制到结构体中的变量的地址;
结构体成员含义如下:
:保留变量;
size:块的大小;
copy:函数用于捕获变量并保存引用;
:析构函数,用于释放捕获的资源;
总结
我们在使用Block的时候需要注意四个关键点:
三种类型的块;块避免循环引用;块对自动变量的复制操作;、、的作用;
提醒:请联系我时一定说明是从茶后生活网上看到的!