MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C代码块(Block)语法结构与内存特性

2023-06-095.3k 阅读

一、Objective-C 代码块(Block)的语法结构

在 Objective-C 中,代码块(Block)是一种带有自动变量(局部变量)的匿名函数。它可以捕获其定义范围内的变量,并将其作为常量在块内部使用。

1.1 基本语法

Block 的基本语法如下:

返回值类型 (^block名称)(参数列表) = ^返回值类型(参数列表) {
    // 代码块执行体
};

例如,定义一个简单的 Block,接受两个 int 类型参数并返回它们的和:

int (^sumBlock)(int, int) = ^int(int a, int b) {
    return a + b;
};
int result = sumBlock(3, 5);
NSLog(@"The sum is %d", result);

在上述代码中,首先定义了一个名为 sumBlock 的 Block,它接受两个 int 类型参数,返回值也是 int 类型。然后通过 sumBlock(3, 5) 调用该 Block,并将结果打印出来。

1.2 省略返回值类型

当 Block 的返回值类型可以从执行体推断出来时,可以省略返回值类型。例如:

// 省略返回值类型
int (^sumBlock)(int, int) = ^(int a, int b) {
    return a + b;
};

编译器能够根据 return a + b 推断出返回值类型为 int

1.3 省略参数列表

如果 Block 没有参数,可以省略参数列表,但是 ^ 符号不能省略。例如:

void (^printHelloBlock)() = ^ {
    NSLog(@"Hello, World!");
};
printHelloBlock();

这里定义了一个没有参数且返回值为 void 的 Block,然后调用它打印出 "Hello, World!"。

1.4 作为函数参数

Block 经常作为函数参数使用。例如,NSArrayenumerateObjectsUsingBlock: 方法就接受一个 Block 作为参数:

NSArray *numbers = @[@1, @2, @3];
[numbers enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSLog(@"Object at index %lu is %@", (unsigned long)idx, obj);
    if (idx == 1) {
        *stop = YES;
    }
}];

在这个例子中,enumerateObjectsUsingBlock: 方法会对数组中的每个元素执行传入的 Block。Block 接受三个参数:当前元素 obj,当前元素的索引 idx,以及一个用于停止枚举的指针 stop。如果索引为 1,就将 *stop 设置为 YES,从而停止枚举。

1.5 捕获变量

Block 可以捕获其定义范围内的变量。例如:

int num = 10;
void (^printNumBlock)() = ^ {
    NSLog(@"The number is %d", num);
};
printNumBlock();

在上述代码中,printNumBlock 捕获了局部变量 num,并在 Block 内部使用它。

二、Objective-C 代码块(Block)的内存特性

理解 Block 的内存特性对于编写高效、稳定的 Objective-C 代码至关重要。Block 在内存中有三种不同的存储类型:栈(Stack)、堆(Heap)和全局数据区(Global Data Area)。

2.1 栈上的 Block

默认情况下,Block 被创建在栈上。栈上的 Block 生命周期与它所在的函数栈帧相同。当函数返回时,栈上的 Block 会被销毁。例如:

void testStackBlock() {
    int num = 10;
    void (^stackBlock)() = ^ {
        NSLog(@"The number is %d", num);
    };
    stackBlock();
}

testStackBlock 函数中,stackBlock 是一个栈上的 Block。它在函数执行期间存在,函数返回后就被销毁。

栈上的 Block 有一些限制。由于栈上的内存是自动管理的,栈上的 Block 不能在其定义的函数返回后继续使用,因为捕获的变量(如 num)可能在函数返回后被销毁。如果尝试这样做,会导致未定义行为。

2.2 堆上的 Block

为了让 Block 能够在其定义的函数返回后继续使用,需要将 Block 从栈上复制到堆上。在 Objective-C 中,可以通过调用 copy 方法将栈上的 Block 复制到堆上。例如:

void (^heapBlock)() = [stackBlock copy];

堆上的 Block 具有动态的生命周期,可以通过 retainrelease(在 ARC 环境下,由编译器自动管理内存)来控制其引用计数。当引用计数为 0 时,堆上的 Block 会被释放。

以下是一个完整的示例,展示了如何将栈上的 Block 复制到堆上,并在函数返回后使用:

void (^createHeapBlock() {
    int num = 10;
    void (^stackBlock)() = ^ {
        NSLog(@"The number is %d", num);
    };
    return [stackBlock copy];
}

int main() {
    void (^heapBlock)() = createHeapBlock();
    heapBlock();
    return 0;
}

在这个例子中,createHeapBlock 函数返回一个堆上的 Block。在 main 函数中,调用 createHeapBlock 并将返回的 Block 赋值给 heapBlock,然后调用 heapBlock 成功打印出结果,证明堆上的 Block 可以在函数返回后继续使用。

2.3 全局数据区的 Block

当 Block 不捕获任何自动变量(局部变量)时,它会被存储在全局数据区。全局数据区的 Block 生命周期与程序相同,在程序启动时创建,程序结束时销毁。例如:

void (^globalBlock)() = ^ {
    NSLog(@"This is a global block");
};

由于 globalBlock 没有捕获任何自动变量,它被存储在全局数据区。全局数据区的 Block 不需要进行 copy 操作,因为它的生命周期不受函数调用的影响。

2.4 Block 对捕获变量的内存影响

当 Block 捕获自动变量时,会对这些变量的内存管理产生影响。

对于基本数据类型(如 intfloat 等),Block 会对其进行值捕获。也就是说,Block 内部使用的是捕获变量的副本,对副本的修改不会影响外部变量。例如:

int num = 10;
void (^block)() = ^ {
    num = 20;
    NSLog(@"In block, num is %d", num);
};
block();
NSLog(@"Outside block, num is %d", num);

输出结果为:

In block, num is 20
Outside block, num is 10

可以看到,在 Block 内部修改 num 不会影响外部的 num

对于对象类型,默认情况下也是值捕获,但是捕获的是对象的指针。如果对象的引用计数发生变化,可能会影响到 Block 内部和外部对该对象的访问。例如:

NSMutableString *string = [NSMutableString stringWithString:@"Hello"];
void (^block)() = ^ {
    [string appendString:@", World!"];
    NSLog(@"In block, string is %@", string);
};
block();
NSLog(@"Outside block, string is %@", string);

输出结果为:

In block, string is Hello, World!
Outside block, string is Hello, World!

在这个例子中,虽然 Block 捕获的是 string 的指针,但由于 NSMutableString 是可变对象,在 Block 内部对其修改会影响到外部。

在 ARC 环境下,Block 对对象类型的捕获会自动管理对象的引用计数。当 Block 被释放时,它捕获的对象也会相应地减少引用计数。

三、循环引用问题及解决方法

在使用 Block 时,一个常见的问题是循环引用。循环引用会导致内存泄漏,因为对象之间相互持有对方的引用,使得它们的引用计数永远不会为 0,从而无法被释放。

3.1 循环引用示例

假设有一个视图控制器 ViewController,在其内部定义了一个 Block,并且 Block 捕获了 self

@interface ViewController ()

@property (nonatomic, copy) void (^block)();

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.block = ^{
        NSLog(@"ViewController: %@", self);
    };
    self.block();
}

@end

在这个例子中,self.block 捕获了 self,而 self 又持有 self.block,形成了循环引用。如果 ViewController 被释放,由于 self.block 持有 selfself 无法被释放;同时,由于 self 持有 self.blockself.block 也无法被释放,导致内存泄漏。

3.2 解决循环引用的方法

  1. 使用 __weak 修饰符(ARC 环境) 在 ARC 环境下,可以使用 __weak 修饰符来打破循环引用。__weak 修饰的变量不会增加对象的引用计数。例如:
@interface ViewController ()

@property (nonatomic, copy) void (^block)();

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            NSLog(@"ViewController: %@", strongSelf);
        }
    };
    self.block();
}

@end

在上述代码中,首先定义了一个 __weak 类型的 weakSelf 来指向 self。在 Block 内部,通过 __strong 修饰的 strongSelf 来临时持有 weakSelf。这样做的好处是,在 Block 执行期间,strongSelf 会保持 self 的引用,确保 self 不会在 Block 执行过程中被释放。同时,由于 weakSelf 不会增加 self 的引用计数,打破了循环引用。

  1. 使用 __block 修饰符(MRC 环境) 在 MRC(手动引用计数)环境下,可以使用 __block 修饰符来解决循环引用问题。__block 修饰的变量在 Block 内部是可变的,并且不会增加对象的引用计数。例如:
@interface ViewController ()

@property (nonatomic, copy) void (^block)();

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block ViewController *blockSelf = self;
    self.block = ^{
        NSLog(@"ViewController: %@", blockSelf);
        blockSelf = nil;
    };
    self.block();
}

@end

在这个例子中,使用 __block 修饰 blockSelf。在 Block 内部,当 blockSelf 不再需要时,可以将其设置为 nil,从而打破循环引用。需要注意的是,在 ARC 环境下,__block 修饰对象类型变量时,仍然会增加对象的引用计数,因此在 ARC 环境下不适合使用 __block 来解决循环引用问题。

四、Block 与 GCD(Grand Central Dispatch)

GCD 是 Apple 开发的一种基于队列的异步编程模型,它与 Block 紧密结合,使得异步编程变得更加简单和高效。

4.1 GCD 队列

GCD 中有两种类型的队列:串行队列(Serial Queue)和并行队列(Concurrent Queue)。此外,还有一个特殊的主队列(Main Queue),它是一个串行队列,用于在主线程上执行任务。

  1. 创建串行队列 可以使用 dispatch_queue_create 函数创建一个串行队列:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", NULL);
  1. 创建并行队列 使用 dispatch_queue_create 函数并传入 DISPATCH_QUEUE_CONCURRENT 标志可以创建一个并行队列:
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
  1. 获取主队列 可以使用 dispatch_get_main_queue 函数获取主队列:
dispatch_queue_t mainQueue = dispatch_get_main_queue();

4.2 在队列中执行 Block

  1. 异步执行 使用 dispatch_async 函数可以将 Block 提交到指定的队列中异步执行。例如,将一个任务提交到并行队列中执行:
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    // 异步执行的任务
    NSLog(@"Task is running asynchronously");
});
  1. 同步执行 使用 dispatch_sync 函数可以将 Block 提交到指定的队列中同步执行。同步执行意味着当前线程会等待 Block 执行完毕后才继续执行。例如,将一个任务提交到串行队列中同步执行:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", NULL);
dispatch_sync(serialQueue, ^{
    // 同步执行的任务
    NSLog(@"Task is running synchronously");
});

需要注意的是,在主队列中使用 dispatch_sync 会导致死锁,因为 dispatch_sync 会阻塞当前线程,而主队列是在主线程上执行的,这样会导致主线程被阻塞,无法处理其他任务,包括 dispatch_sync 提交的任务。

4.3 队列组(Dispatch Group)

队列组可以用于将多个任务提交到不同的队列中执行,并在所有任务完成后执行一个完成 Block。例如:

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_async(group, concurrentQueue, ^{
    // 第一个任务
    NSLog(@"Task 1 is running");
});

dispatch_group_async(group, concurrentQueue, ^{
    // 第二个任务
    NSLog(@"Task 2 is running");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 所有任务完成后执行的 Block
    NSLog(@"All tasks are completed");
});

在上述代码中,首先创建了一个队列组 group 和一个并行队列 concurrentQueue。然后使用 dispatch_group_async 将两个任务提交到并行队列中执行。最后,使用 dispatch_group_notify 定义了一个完成 Block,当所有任务完成后,该 Block 会在主队列中执行。

通过结合 Block 和 GCD,可以实现高效的异步编程,充分利用多核处理器的性能,提高应用程序的响应性和效率。

五、Block 的高级用法

除了上述基本用法外,Block 在 Objective-C 中还有一些高级用法。

5.1 递归 Block

Block 可以是递归的,即 Block 内部可以调用自身。例如,计算阶乘的递归 Block:

int (^factorialBlock)(int) = ^(int n) {
    if (n == 0 || n == 1) {
        return 1;
    } else {
        return n * factorialBlock(n - 1);
    }
};
int result = factorialBlock(5);
NSLog(@"The factorial of 5 is %d", result);

在这个例子中,factorialBlock 是一个递归 Block,它通过不断调用自身来计算阶乘。

5.2 带可变参数的 Block

虽然 Objective-C 的 Block 本身不直接支持可变参数列表,但可以通过 va_list 来实现类似功能。例如:

#import <stdarg.h>

void (^printNumbersBlock)(int, ...) = ^(int count, ...) {
    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; i++) {
        int num = va_arg(args, int);
        NSLog(@"Number %d: %d", i + 1, num);
    }
    va_end(args);
};

printNumbersBlock(3, 10, 20, 30);

在上述代码中,printNumbersBlock 接受一个整数 count 表示参数的数量,然后通过 va_list 来获取可变参数列表并打印出来。

5.3 嵌套 Block

Block 可以嵌套定义,即在一个 Block 内部定义另一个 Block。例如:

void (^outerBlock)() = ^ {
    NSLog(@"Outer block starts");
    void (^innerBlock)() = ^ {
        NSLog(@"Inner block");
    };
    innerBlock();
    NSLog(@"Outer block ends");
};
outerBlock();

在这个例子中,outerBlock 内部定义了 innerBlock,并在 outerBlock 执行过程中调用了 innerBlock

通过这些高级用法,可以进一步发挥 Block 的灵活性和强大功能,满足复杂的编程需求。

综上所述,Objective-C 的代码块(Block)具有丰富的语法结构和独特的内存特性。合理使用 Block 不仅可以使代码更加简洁和易读,还能通过与 GCD 等技术结合实现高效的异步编程。同时,深入理解 Block 的内存管理和循环引用问题,对于编写高质量、无内存泄漏的 Objective-C 代码至关重要。在实际开发中,应根据具体需求选择合适的 Block 用法,并注意内存管理和避免循环引用等问题。