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的时候需要注意四个关键点:

三种类型的块;块避免循环引用;块对自动变量的复制操作;、、的作用;

同城信息网

提醒:请联系我时一定说明是从茶后生活网上看到的!