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

Objective-C 中 Block 的使用与高级技巧

2024-06-115.1k 阅读

一、Block 基础概念

在 Objective - C 中,Block 是一种带有自动变量(局部变量)的匿名函数。它可以作为一个值进行传递,存储在变量中,或者作为参数传递给其他函数,甚至可以从函数中返回。

1.1 Block 的定义与语法

Block 的基本语法形式如下:

returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
    // 执行代码块
    statements;
    return value;
};

其中,returnType 是 Block 的返回值类型,blockName 是 Block 的名称,parameterTypesparameters 分别是 Block 的参数类型和参数列表。如果 Block 没有返回值,returnType 可以省略,或者写为 void;如果 Block 没有参数,(parameterTypes)(parameters) 可以省略。

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

int (^sumBlock)(int, int) = ^int(int a, int b) {
    return a + b;
};

这里定义了一个名为 sumBlock 的 Block,它接受两个 int 类型的参数,并返回一个 int 类型的值。可以通过以下方式调用这个 Block:

int result = sumBlock(3, 5);
NSLog(@"The sum is %d", result);

上述代码会输出 The sum is 8

1.2 Block 与函数指针的区别

虽然 Block 看起来有点像函数指针,但它们有本质的区别。函数指针指向一个函数的入口地址,而 Block 不仅包含代码,还可以捕获其定义时所在作用域中的自动变量(局部变量)。

例如:

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

在上述代码中,printBlock 在定义时捕获了 num 变量。尽管之后 num 的值被修改为 20,但当调用 printBlock 时,输出的仍然是 10,这是因为 Block 捕获的是变量的副本。

二、Block 的使用场景

2.1 作为回调函数

在很多异步操作中,如网络请求、文件读取等,Block 常被用作回调函数。例如,在 iOS 开发中使用 NSURLSession 进行网络请求时:

NSURL *url = [NSURL URLWithString:@"https://example.com"];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    if (!error && data) {
        // 处理响应数据
        NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"Response: %@", responseString);
    } else {
        NSLog(@"Error: %@", error);
    }
}];
[task resume];

在这个例子中,completionHandler 是一个 Block,当网络请求完成时,系统会调用这个 Block,并将请求得到的数据、响应以及可能出现的错误作为参数传递进来。

2.2 集合遍历

在处理集合(如 NSArrayNSDictionary)时,Block 提供了一种简洁的遍历方式。

对于 NSArray

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

上述代码使用 enumerateObjectsUsingBlock: 方法遍历 numbers 数组。在 Block 中,obj 是当前遍历到的对象,idx 是对象的索引,stop 是一个指针,通过设置 *stop = YES 可以提前终止遍历。

对于 NSDictionary

NSDictionary *dict = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
[dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
    NSLog(@"Key: %@, Value: %@", key, obj);
}];

这里使用 enumerateKeysAndObjectsUsingBlock: 方法遍历字典,key 是字典的键,obj 是对应的值。

三、Block 的内存管理

3.1 Block 的存储域

在 Objective - C 中,Block 有三种存储域:栈(Stack)、堆(Heap)和全局区(Global)。

  • 栈 Block:当 Block 在函数内部定义,且没有被 copy 操作时,它存储在栈上。栈 Block 的生命周期与函数栈帧相同,函数结束时,栈 Block 会被释放。例如:
void testStackBlock() {
    int num = 10;
    void (^stackBlock)() = ^{
        NSLog(@"The number is %d", num);
    };
    stackBlock();
}

在这个例子中,stackBlock 是一个栈 Block,它在 testStackBlock 函数内部定义,并且没有被 copy。

  • 堆 Block:通过 copy 操作,栈 Block 会被复制到堆上,成为堆 Block。堆 Block 的生命周期由引用计数管理,当引用计数为 0 时,堆 Block 会被释放。例如:
void testHeapBlock() {
    int num = 10;
    void (^stackBlock)() = ^{
        NSLog(@"The number is %d", num);
    };
    void (^heapBlock)() = [stackBlock copy];
    stackBlock = nil;
    heapBlock();
}

这里,stackBlock 是栈 Block,通过 copy 操作创建了 heapBlockheapBlock 是堆 Block。即使 stackBlock 被置为 nilheapBlock 仍然可以正常调用。

  • 全局 Block:当 Block 不捕获任何自动变量(局部变量)时,它存储在全局区。全局 Block 的生命周期与应用程序相同。例如:
void (^globalBlock)() = ^{
    NSLog(@"This is a global block");
};

globalBlock 没有捕获任何局部变量,所以它是全局 Block。

3.2 Block 对对象的引用

当 Block 捕获对象类型的自动变量时,会对该对象产生强引用。例如:

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
@end

void testBlockObjectCapture() {
    Person *person = [[Person alloc] init];
    person.name = @"John";
    void (^block)() = ^{
        NSLog(@"The person's name is %@", person.name);
    };
    block();
    person = nil;
    block();
}

在这个例子中,block 捕获了 person 对象,对 person 产生了强引用。即使在 person = nil 之后,block 仍然可以正常访问 person 的属性,因为 block 持有 person 的引用。

为了避免循环引用(如在 self 和 Block 之间),可以使用 __weak 修饰符。例如:

@interface ViewController : UIViewController
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf) {
            strongSelf.title = @"New Title";
        }
    });
}
@end

这里,首先使用 __weak 修饰符创建了一个弱引用 weakSelf,在 Block 内部,通过 __strong 修饰符将弱引用提升为强引用 strongSelf,这样可以确保在 Block 执行期间 self 不会被释放,同时避免了循环引用。

四、Block 的高级技巧

4.1 Block 作为函数参数的变化

在 Objective - C 中,Block 作为函数参数时,其声明语法有一些变化。例如,定义一个函数,接受一个 Block 作为参数:

void executeBlock(void (^block)()) {
    block();
}

这里,executeBlock 函数接受一个没有参数和返回值的 Block。调用这个函数的方式如下:

void (^myBlock)() = ^{
    NSLog(@"This is my block");
};
executeBlock(myBlock);

如果 Block 有参数和返回值,函数声明会相应变化。例如,接受一个接受两个整数并返回它们乘积的 Block:

int executeMultiplyBlock(int (^block)(int, int), int a, int b) {
    return block(a, b);
}

调用这个函数:

int (^multiplyBlock)(int, int) = ^int(int a, int b) {
    return a * b;
};
int result = executeMultiplyBlock(multiplyBlock, 3, 5);
NSLog(@"The product is %d", result);

4.2 Block 与协议

可以在协议中定义 Block 类型的方法。例如:

@protocol MyProtocol <NSObject>
- (void)performActionWithBlock:(void (^)(void))block;
@end

@interface MyClass : NSObject <MyProtocol>
@end

@implementation MyClass
- (void)performActionWithBlock:(void (^)(void))block {
    block();
}
@end

在这个例子中,MyProtocol 协议定义了一个接受 Block 作为参数的方法 performActionWithBlock:MyClass 实现了这个协议,并在实现中调用了传入的 Block。

4.3 递归 Block

Block 也可以实现递归。例如,计算阶乘的递归 Block:

NSInteger (^factorialBlock)(NSInteger) = ^NSInteger(NSInteger num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * factorialBlock(num - 1);
    }
};
NSInteger result = factorialBlock(5);
NSLog(@"The factorial of 5 is %ld", (long)result);

这里,factorialBlock 是一个递归 Block,它在自身内部调用自己来计算阶乘。

4.4 可变参数 Block

在某些情况下,可能需要定义接受可变参数的 Block。虽然 Objective - C 本身没有直接支持可变参数 Block 的语法,但可以通过 va_list 来实现类似功能。例如:

void (^printNumbersBlock)(...) = ^(va_list args) {
    while (true) {
        NSNumber *number = va_arg(args, NSNumber *);
        if (!number) {
            break;
        }
        NSLog(@"%@", number);
    }
};

va_list args;
va_start(args, nil);
va_arg(args, NSNumber *);
va_arg(args, NSNumber *);
va_end(args);
printNumbersBlock(args);

在这个例子中,printNumbersBlock 是一个通过 va_list 模拟可变参数的 Block。首先通过 va_start 初始化 va_list,然后使用 va_arg 逐个获取参数,最后通过 va_end 结束。

五、Block 与 GCD(Grand Central Dispatch)

5.1 GCD 简介

GCD 是 Apple 开发的一种基于队列的高效异步编程模型,它在多核处理器上可以充分利用硬件资源,提高应用程序的性能。GCD 中的任务以 Block 的形式提交到队列中执行。

5.2 队列与 Block

GCD 中有两种类型的队列:串行队列和并发队列。

  • 串行队列:任务按照提交的顺序依次执行。可以通过 dispatch_queue_create 创建自定义串行队列:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
    // 任务 1
    NSLog(@"Task 1 in serial queue");
});
dispatch_async(serialQueue, ^{
    // 任务 2
    NSLog(@"Task 2 in serial queue");
});

在这个例子中,Task 1 会先执行,然后 Task 2 执行。

  • 并发队列:任务可以并发执行,但仍然按照提交的顺序开始执行。系统提供了全局并发队列,可以通过 dispatch_get_global_queue 获取:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    // 任务 1
    NSLog(@"Task 1 in global queue");
});
dispatch_async(globalQueue, ^{
    // 任务 2
    NSLog(@"Task 2 in global queue");
});

这里,Task 1Task 2 可能会并发执行,具体取决于系统资源和调度。

5.3 同步与异步执行

在 GCD 中,可以使用 dispatch_syncdispatch_async 来提交任务。

  • 异步执行(dispatch_async:将任务提交到队列后,立即返回,不会阻塞当前线程。例如:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    // 异步任务
    NSLog(@"Asynchronous task");
});
NSLog(@"Main thread continues");

在这个例子中,dispatch_async 将任务提交到全局队列后,主线程继续执行,NSLog(@"Main thread continues") 会先输出,然后异步任务中的 NSLog(@"Asynchronous task") 输出。

  • 同步执行(dispatch_sync:将任务提交到队列后,会等待任务执行完毕才返回,会阻塞当前线程。例如:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
    // 同步任务
    NSLog(@"Synchronous task");
});
NSLog(@"Main thread continues");

这里,dispatch_sync 会阻塞主线程,直到同步任务执行完毕,所以 NSLog(@"Synchronous task") 会先输出,然后 NSLog(@"Main thread continues") 输出。

5.4 队列组(Dispatch Group)

队列组可以用来管理一组任务,当所有任务执行完毕后执行特定操作。例如:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{
    // 任务 1
    NSLog(@"Task 1 in group");
});
dispatch_group_async(group, queue, ^{
    // 任务 2
    NSLog(@"Task 2 in group");
});

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

在这个例子中,dispatch_group_async 将任务提交到队列并加入队列组,dispatch_group_notify 会在所有任务完成后,在主线程执行指定的操作。

六、Block 与多线程

6.1 Block 与线程安全

在多线程环境下使用 Block 时,需要注意线程安全问题。如果多个线程同时访问和修改 Block 捕获的共享资源,可能会导致数据竞争和不一致。

例如,假设多个线程同时调用一个 Block,该 Block 对一个共享变量进行自增操作:

int sharedVariable = 0;
void (^incrementBlock)() = ^{
    sharedVariable++;
};

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, incrementBlock);
}

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"Shared variable value: %d", sharedVariable);
});

在上述代码中,如果不采取任何线程同步措施,最终 sharedVariable 的值可能不是 1000,因为多个线程同时访问和修改它会导致数据竞争。

6.2 使用锁保证线程安全

为了保证线程安全,可以使用锁来保护共享资源。例如,使用 @synchronized 关键字:

int sharedVariable = 0;
void (^incrementBlock)() = ^{
    @synchronized(self) {
        sharedVariable++;
    }
};

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, incrementBlock);
}

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"Shared variable value: %d", sharedVariable);
});

在这个例子中,@synchronized(self) 确保了在同一时间只有一个线程可以进入代码块修改 sharedVariable,从而保证了线程安全。

除了 @synchronized,还可以使用 NSLockpthread_mutex 等其他锁机制来保证线程安全。

七、Block 的性能优化

7.1 减少 Block 的创建开销

Block 的创建和销毁都有一定的开销。在性能敏感的代码中,应尽量减少不必要的 Block 创建。例如,如果一个 Block 在循环中被多次创建,可以将其提取到循环外部。

例如,原本在循环中创建 Block:

for (int i = 0; i < 1000; i++) {
    void (^block)() = ^{
        NSLog(@"Iteration %d", i);
    };
    block();
}

优化后,将 Block 提取到循环外部:

void (^block)() = ^{
    NSLog(@"Iteration %d", i);
};
for (int i = 0; i < 1000; i++) {
    block();
}

这样可以减少 Block 的创建开销,提高性能。

7.2 避免循环引用导致的内存泄漏

如前文所述,循环引用会导致内存泄漏,从而影响性能。通过使用 __weak 修饰符来打破循环引用是非常重要的。在使用 Block 时,尤其是在对象的属性中使用 Block,要仔细检查是否存在循环引用的可能。

例如,在一个视图控制器中,如果存在以下情况:

@interface ViewController : UIViewController
@property (nonatomic, copy) void (^block)();
@end

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

这里就存在 selfblock 之间的循环引用。应该使用 __weak 修饰符来打破循环:

@interface ViewController : UIViewController
@property (nonatomic, copy) void (^block)();
@end

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

这样可以避免因循环引用导致的内存泄漏,提高应用程序的性能。

7.3 合理使用 GCD 队列

在使用 Block 与 GCD 时,合理选择队列类型对性能有很大影响。对于 I/O 密集型任务,通常使用并发队列可以提高效率,因为 I/O 操作往往需要等待外部设备响应,并发执行可以充分利用等待时间。而对于 CPU 密集型任务,如果任务之间相互独立,可以考虑使用并发队列;如果任务之间存在依赖关系,可能需要使用串行队列。

例如,在进行文件读取操作时:

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
    NSData *data = [NSData dataWithContentsOfFile:@"path/to/file"];
    if (data) {
        // 处理数据
    }
});

这里使用并发队列可以在文件读取等待时执行其他任务,提高整体性能。

八、总结 Block 的最佳实践

  1. 明确 Block 的作用:在使用 Block 之前,清楚它是作为回调函数、遍历集合还是用于其他目的,这样可以更好地设计 Block 的参数和返回值。
  2. 注意内存管理:了解 Block 的存储域,避免循环引用,尤其是在涉及对象引用时。使用 __weak 修饰符来打破循环引用,确保内存的正确释放。
  3. 考虑性能优化:减少不必要的 Block 创建,合理选择 GCD 队列以提高性能。在多线程环境下,确保 Block 访问共享资源的线程安全。
  4. 保持代码清晰:Block 的语法虽然灵活,但也要注意保持代码的可读性。给 Block 命名时,尽量使用有意义的名称,并且在复杂的 Block 内部添加注释。

通过遵循这些最佳实践,可以更好地在 Objective - C 项目中使用 Block,提高代码的质量和性能。

Block 作为 Objective - C 中强大的特性,为开发者提供了灵活、高效的编程方式。深入理解和掌握 Block 的使用与高级技巧,对于开发高质量的 iOS 和 macOS 应用程序至关重要。无论是异步编程、集合操作还是多线程处理,Block 都发挥着重要的作用。希望通过本文的介绍,读者能够更加熟练地运用 Block 来解决实际开发中的问题。