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

深入理解Objective-C中的Block语法与特性

2021-11-212.6k 阅读

一、Block 的基本概念

在 Objective-C 中,Block 是一种带有自动变量(局部变量)的匿名函数。它可以作为一种数据类型进行传递、存储和调用,为开发者提供了一种灵活且强大的编程方式。

从本质上来说,Block 是一个对象,它封装了一段代码以及该代码执行时所需的上下文环境。这意味着 Block 不仅包含了要执行的代码逻辑,还能记住其定义时所在作用域中的局部变量的值。

二、Block 的语法

2.1 定义 Block

Block 的定义语法与函数定义有相似之处,但也有其独特的地方。其基本语法形式如下:

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

例如,定义一个简单的 Block,用于计算两个整数的和:

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

在上述代码中,int (^sumBlock)(int, int) 定义了一个名为 sumBlock 的 Block,它接受两个 int 类型的参数,并返回一个 int 类型的值。^int(int a, int b) 则是 Block 的具体实现部分,其中 ^ 符号表示这是一个 Block,int 是返回值类型,(int a, int b) 是参数列表。

2.2 调用 Block

定义好 Block 后,调用方式与函数调用类似:

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

这里通过 sumBlock(3, 5) 调用了之前定义的 Block,并将返回值赋给 result 变量,然后输出结果。

2.3 省略返回值类型

在很多情况下,Block 的返回值类型可以省略,编译器能够根据 Block 中的 return 语句自动推断返回值类型。例如:

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

这种写法更加简洁,尤其是在返回值类型较为明显的情况下。

2.4 无参数和无返回值的 Block

对于无参数且无返回值的 Block,其定义和使用如下:

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

这里定义了一个 printHelloBlock,它没有参数,也不返回任何值,调用该 Block 会在控制台输出 "Hello, Block!"。

三、Block 与自动变量(局部变量)

3.1 捕获自动变量的值

Block 能够捕获其定义时所在作用域中的自动变量的值。例如:

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

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

3.2 对自动变量的修改

默认情况下,Block 内部不能修改捕获的自动变量的值。例如:

int num = 10;
void (^modifyNumBlock)() = ^{
    num = 20; // 编译错误
};

上述代码会导致编译错误,提示 "Variable is not assignable (missing __block type specifier)"。要在 Block 内部修改自动变量的值,需要使用 __block 修饰符。

3.3 __block 修饰符

使用 __block 修饰符可以让 Block 内部修改捕获的自动变量的值。例如:

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

在这个例子中,通过 __block 修饰 num 变量,使得 modifyNumBlock 能够修改 num 的值。调用 modifyNumBlock 后,num 的值变为 20,并通过 NSLog 输出。

四、Block 的类型

4.1 NSGlobalBlock

当 Block 没有捕获任何自动变量时,它属于 __NSGlobalBlock__ 类型。这种类型的 Block 存储在程序的全局数据区,类似于全局函数。例如:

void (^globalBlock)() = ^{
    NSLog(@"This is a global block.");
};
NSLog(@"%@", NSStringFromClass([globalBlock class]));

运行上述代码,会输出 __NSGlobalBlock__,表明 globalBlock 是一个全局 Block。

4.2 NSStackBlock

如果 Block 捕获了自动变量,但还没有被拷贝到堆上,它属于 __NSStackBlock__ 类型。这种类型的 Block 存储在栈上,其生命周期与栈上的局部变量类似。例如:

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

testStackBlock 函数中定义的 stackBlock 捕获了局部变量 num,此时它是 __NSStackBlock__ 类型。当 testStackBlock 函数执行完毕,stackBlock 也会随着栈帧的销毁而被释放。

4.3 NSMallocBlock

__NSStackBlock__ 类型的 Block 被拷贝到堆上时,它会变成 __NSMallocBlock__ 类型。这种类型的 Block 存储在堆上,其生命周期由程序员手动管理(通过 copyrelease 操作)。通常,当将 Block 作为方法参数传递或者赋值给一个属性时,会自动将栈上的 Block 拷贝到堆上。例如:

@interface MyClass : NSObject
@property (nonatomic, copy) void (^heapBlock)();
@end

@implementation MyClass
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int num = 10;
        MyClass *obj = [[MyClass alloc] init];
        obj.heapBlock = ^{
            NSLog(@"The number is: %d", num);
        };
        NSLog(@"%@", NSStringFromClass([obj.heapBlock class]));
    }
    return 0;
}

在上述代码中,obj.heapBlock__NSMallocBlock__ 类型,因为通过属性赋值,栈上的 Block 被自动拷贝到了堆上。

五、Block 作为方法参数

5.1 简单的 Block 参数示例

在 Objective-C 中,经常会将 Block 作为方法参数传递,以实现更灵活的功能。例如,定义一个方法,该方法接受一个 Block 作为参数,并在方法内部调用这个 Block:

@interface MathUtils : NSObject
+ (void)performOperationWithBlock:(void (^)())operationBlock;
@end

@implementation MathUtils
+ (void)performOperationWithBlock:(void (^)())operationBlock {
    operationBlock();
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [MathUtils performOperationWithBlock:^{
            NSLog(@"Performing an operation in the block.");
        }];
    }
    return 0;
}

在这个例子中,MathUtils 类的 performOperationWithBlock: 方法接受一个无参数无返回值的 Block。在 main 函数中,通过传递一个 Block 给该方法,实现了在 performOperationWithBlock: 方法内部执行特定的代码逻辑。

5.2 带有参数和返回值的 Block 参数

Block 作为方法参数也可以带有参数和返回值。例如,定义一个方法,接受两个整数和一个计算 Block,返回计算结果:

@interface MathUtils : NSObject
+ (int)calculateWithA:(int)a b:(int)b operation:(int (^)(int, int))operationBlock;
@end

@implementation MathUtils
+ (int)calculateWithA:(int)a b:(int)b operation:(int (^)(int, int))operationBlock {
    return operationBlock(a, b);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int result = [MathUtils calculateWithA:3 b:5 operation:^(int a, int b) {
            return a + b;
        }];
        NSLog(@"The result is: %d", result);
    }
    return 0;
}

这里 calculateWithA:b:operation: 方法接受两个整数参数 ab,以及一个接受两个 int 类型参数并返回 int 类型值的 Block。在 main 函数中,通过传递具体的计算 Block,实现了对两个整数的加法运算。

六、Block 与内存管理

6.1 Block 的内存分配

如前文所述,Block 有三种类型:__NSGlobalBlock____NSStackBlock____NSMallocBlock____NSGlobalBlock__ 存储在全局数据区,生命周期与程序相同;__NSStackBlock__ 存储在栈上,其生命周期与栈上的局部变量一致;__NSMallocBlock__ 存储在堆上,需要手动管理内存。

6.2 对 Block 的 copy 操作

当需要将 Block 从栈上拷贝到堆上时,通常会使用 copy 操作。例如,将一个栈上的 Block 赋值给一个 copy 类型的属性时,系统会自动对 Block 进行 copy 操作,将其从栈上拷贝到堆上。另外,也可以显式地对 Block 进行 copy 操作:

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

在上述代码中,通过 [stackBlock copy]stackBlock__NSStackBlock__ 类型)拷贝到堆上,得到 heapBlock__NSMallocBlock__ 类型)。

6.3 Block 对捕获变量的内存管理影响

当 Block 捕获对象类型的自动变量时,会对该对象进行持有(retain)操作。例如:

NSString *str = @"Hello";
void (^block)() = ^{
    NSLog(@"%@", str);
};

在这个例子中,block 捕获了 str,此时 block 会对 str 进行持有。当 block 被释放时,会自动释放对 str 的持有。

如果使用 __block 修饰对象类型的变量,情况会有所不同。例如:

__block NSString *str = @"Hello";
void (^block)() = ^{
    str = @"World";
};

在这种情况下,block 并不会直接持有 str,而是通过一个中间结构来间接引用 str。当 blockstr 进行修改时,会涉及到更复杂的内存管理操作。

七、Block 与多线程

7.1 在多线程中使用 Block

Block 在多线程编程中非常有用。例如,可以使用 dispatch_async 函数将 Block 提交到一个异步队列中执行,实现多线程操作。以下是一个简单的示例:

#import <dispatch/dispatch.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_async(queue, ^{
            NSLog(@"This is a task running asynchronously.");
        });
        NSLog(@"Main thread continues execution.");
    }
    return 0;
}

在上述代码中,dispatch_get_global_queue 获取一个全局的异步队列,dispatch_async 将一个 Block 提交到该队列中执行。主线程不会等待 Block 执行完毕,而是继续执行后续代码,从而实现了异步操作。

7.2 注意事项

在多线程环境下使用 Block 时,需要注意线程安全问题。例如,如果多个线程同时访问和修改 Block 捕获的共享变量,可能会导致数据竞争和未定义行为。为了避免这些问题,可以使用锁机制或者其他线程同步工具。

另外,由于 Block 可能会在不同的线程中执行,需要确保 Block 内部涉及的资源(如文件句柄、网络连接等)在多线程环境下的正确性和稳定性。

八、Block 的高级应用

8.1 链式调用

利用 Block 可以实现链式调用,使代码更加简洁和易读。例如,定义一个简单的数学计算类,通过 Block 实现链式调用:

@interface MathChain : NSObject
@property (nonatomic, assign) int result;
- (MathChain *(^)(int))add;
- (MathChain *(^)(int))subtract;
@end

@implementation MathChain
- (MathChain *(^)(int))add {
    return ^(int num) {
        self.result += num;
        return self;
    };
}

- (MathChain *(^)(int))subtract {
    return ^(int num) {
        self.result -= num;
        return self;
    };
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MathChain *mathChain = [[MathChain alloc] init];
        mathChain.result = 10;
        [mathChain add(5) subtract(3)];
        NSLog(@"The final result is: %d", mathChain.result);
    }
    return 0;
}

在这个例子中,MathChain 类的 addsubtract 方法返回一个 Block,该 Block 可以继续调用其他方法,从而实现了链式调用。

8.2 回调机制

Block 常用于实现回调机制。例如,在网络请求中,当请求完成后通过 Block 回调返回结果:

#import <AFNetworking/AFNetworking.h>

void fetchDataWithCompletionBlock(void (^completionBlock)(NSData *, NSError *)) {
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    [manager GET:@"http://example.com/api/data" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSData *data = [NSJSONSerialization dataWithJSONObject:responseObject options:NSJSONWritingPrettyPrinted error:nil];
        completionBlock(data, nil);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        completionBlock(nil, error);
    }];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        fetchDataWithCompletionBlock(^(NSData *data, NSError *error) {
            if (data) {
                NSLog(@"Data fetched successfully: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
            } else {
                NSLog(@"Error: %@", error);
            }
        });
    }
    return 0;
}

在上述代码中,fetchDataWithCompletionBlock 方法接受一个 Block 作为参数,在网络请求完成后,根据请求结果调用该 Block,将数据或错误信息传递给回调 Block。

九、Block 与其他语言特性的对比

9.1 与函数指针的对比

在 C 语言中,函数指针也可以实现类似 Block 的功能,用于传递一段代码。然而,函数指针不能捕获其定义时所在作用域中的自动变量的值。而 Block 不仅可以像函数指针一样传递代码,还能记住其定义时的上下文环境,这使得 Block 在功能上更加灵活和强大。

9.2 与闭包的对比

在其他一些编程语言(如 Swift、JavaScript 等)中,闭包与 Objective-C 的 Block 有相似之处。闭包也是一种能够捕获其定义时所在作用域中的变量的匿名函数。然而,不同语言的闭包在语法、内存管理等方面可能存在差异。例如,在 Swift 中,闭包的语法更加简洁,并且在内存管理方面有自己独特的机制,而 Objective-C 的 Block 与 Objective-C 的内存管理体系(如引用计数)紧密结合。

十、总结

通过对 Objective-C 中 Block 语法与特性的深入探讨,我们了解到 Block 作为一种强大的编程工具,在 Objective-C 编程中具有广泛的应用。从基本的定义、语法使用,到与自动变量的交互、内存管理、多线程应用以及高级应用场景,Block 为开发者提供了丰富的功能和灵活的编程方式。在实际开发中,合理运用 Block 可以使代码更加简洁、高效,并且易于维护。同时,深入理解 Block 的特性和原理,也有助于开发者避免在使用过程中出现的各种潜在问题,如内存泄漏、线程安全等。希望本文的内容能够帮助读者更好地掌握和运用 Objective-C 中的 Block。