学会在Objective-C中使用Block进行代码封装与回调
1. Block简介
在Objective-C编程中,Block是一种带有自动变量(局部变量)的匿名函数。它可以像对象一样被传递、赋值和存储,极大地增强了代码的灵活性和可读性。Block在内存管理方面也有其独特的机制,这使得它在各种场景下都能高效运行。
从语法角度看,Block的定义形式如下:
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
// 执行代码
};
例如,定义一个简单的Block用于计算两个整数的和:
int (^sumBlock)(int, int) = ^int(int a, int b) {
return a + b;
};
int result = sumBlock(3, 5);
NSLog(@"Sum result: %d", result);
这里,sumBlock
是一个Block变量,它接受两个int
类型的参数并返回一个int
类型的值。通过调用sumBlock(3, 5)
,我们得到了两个数的和并打印出来。
2. 使用Block进行代码封装
2.1 封装简单逻辑
假设我们有一个需求,在多个地方需要对一个整数进行特定的运算,比如将其乘以2再加1。我们可以使用Block来封装这个逻辑。
int (^customCalculationBlock)(int) = ^int(int num) {
return num * 2 + 1;
};
int number = 5;
int calculatedValue = customCalculationBlock(number);
NSLog(@"Calculated value: %d", calculatedValue);
在这个例子中,customCalculationBlock
封装了对整数的特定运算逻辑。我们只需要定义一次这个Block,然后在需要的地方调用它,传入不同的整数参数,就能得到相应的计算结果。这样做不仅使代码更加简洁,而且便于维护和修改。如果后续需求发生变化,比如将运算改为乘以3再减1,我们只需要修改Block内部的代码,而不需要在所有调用该运算的地方逐一修改。
2.2 封装复杂业务逻辑
在实际项目中,业务逻辑往往比较复杂。例如,在一个电商应用中,处理订单支付的逻辑可能涉及到与服务器交互验证用户信息、扣除商品库存、更新订单状态等多个步骤。我们可以使用Block来封装这些复杂的业务逻辑。
// 模拟服务器请求成功回调Block
typedef void (^PaymentSuccessBlock)(NSString *orderId);
// 模拟服务器请求失败回调Block
typedef void (^PaymentFailureBlock)(NSError *error);
// 封装支付逻辑的Block
void (^processPaymentBlock)(NSString *paymentInfo, PaymentSuccessBlock successBlock, PaymentFailureBlock failureBlock) = ^void(NSString *paymentInfo, PaymentSuccessBlock successBlock, PaymentFailureBlock failureBlock) {
// 模拟验证用户信息
BOOL isValidUser = [self validateUser];
if (!isValidUser) {
NSError *error = [NSError errorWithDomain:@"PaymentError" code:1 userInfo:nil];
failureBlock(error);
return;
}
// 模拟扣除商品库存
BOOL isStockAvailable = [self deductStock];
if (!isStockAvailable) {
NSError *error = [NSError errorWithDomain:@"PaymentError" code:2 userInfo:nil];
failureBlock(error);
return;
}
// 模拟与服务器交互完成支付
NSString *orderId = [self completePaymentOnServer:paymentInfo];
if (orderId) {
successBlock(orderId);
} else {
NSError *error = [NSError errorWithDomain:@"PaymentError" code:3 userInfo:nil];
failureBlock(error);
}
};
// 调用支付逻辑Block
processPaymentBlock(@"payment details", ^(NSString *orderId) {
NSLog(@"Payment success. Order ID: %@", orderId);
}, ^(NSError *error) {
NSLog(@"Payment failed. Error: %@", error);
});
在这个例子中,processPaymentBlock
封装了整个支付流程的复杂业务逻辑。它接受支付信息、成功回调Block和失败回调Block作为参数。在Block内部,依次进行用户信息验证、库存扣除和服务器支付操作,并根据不同的结果调用相应的回调Block。这样,我们将复杂的支付业务逻辑封装在一个Block中,调用者只需要关心传入必要的参数和处理回调结果,而不需要了解具体的实现细节。
3. Block在回调中的应用
3.1 异步操作回调
在开发中,异步操作非常常见,比如网络请求、文件读取等。使用Block可以方便地处理这些异步操作的回调。以网络请求为例,假设我们使用AFNetworking库(这里仅为示例说明,实际使用中AFNetworking的API可能有所不同)。
#import <AFNetworking/AFNetworking.h>
void (^fetchDataBlock)(void) = ^void() {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"https://example.com/api/data" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"Data fetched successfully: %@", responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"Data fetch failed: %@", error);
}];
};
fetchDataBlock();
在这个例子中,fetchDataBlock
封装了一个网络请求操作。AFNetworking库的GET
方法接受成功和失败回调Block作为参数。当网络请求成功时,成功回调Block会被执行,我们可以在其中处理返回的数据;当请求失败时,失败回调Block会被执行,我们可以在其中处理错误信息。通过这种方式,我们使用Block实现了异步网络请求的回调处理,确保在网络请求过程中,主线程不会被阻塞,应用仍然可以响应用户操作。
3.2 视图交互回调
在iOS应用开发中,视图之间的交互也经常需要使用回调。比如,一个自定义的按钮视图可能需要在用户点击时通知其上级视图进行相应的操作。我们可以使用Block来实现这种视图交互回调。
// 自定义按钮类
@interface CustomButton : UIButton
@property (nonatomic, copy) void (^buttonTapBlock)(void);
@end
@implementation CustomButton
- (void)awakeFromNib {
[super awakeFromNib];
[self addTarget:self action:@selector(handleTap) forControlEvents:UIControlEventTouchUpInside];
}
- (void)handleTap {
if (self.buttonTapBlock) {
self.buttonTapBlock();
}
}
@end
// 在视图控制器中使用自定义按钮
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CustomButton *customButton = [[CustomButton alloc] initWithFrame:CGRectMake(100, 100, 200, 50)];
customButton.buttonTapBlock = ^{
NSLog(@"Custom button was tapped.");
// 在这里可以执行视图控制器的相关操作,比如导航到新的视图
};
[self.view addSubview:customButton];
}
@end
在这个例子中,CustomButton
类定义了一个buttonTapBlock
属性,用于存储按钮点击时要执行的代码块。当按钮被点击时,handleTap
方法会检查buttonTapBlock
是否存在,如果存在则执行它。在视图控制器中,我们创建了一个CustomButton
实例,并为其buttonTapBlock
属性赋值,当按钮被点击时,就会执行我们在Block中定义的代码,实现了从按钮视图到视图控制器的回调。
4. Block的内存管理
4.1 Block的存储类型
在Objective-C中,Block有三种存储类型:栈上(NSStackBlock)、堆上(NSMallocBlock)和全局区(NSGlobalBlock)。
- NSGlobalBlock:当Block没有捕获任何自动变量(局部变量)时,它会被存储在全局区。这种Block的生命周期与应用程序相同,不需要额外的内存管理。例如:
void (^globalBlock)(void) = ^{
NSLog(@"This is a global block.");
};
这里的globalBlock
没有捕获任何局部变量,所以它是一个NSGlobalBlock
,存储在全局区。
- NSStackBlock:当Block捕获了自动变量时,默认情况下它会被存储在栈上。栈上的Block生命周期与定义它的函数的栈帧相同,当函数返回时,栈上的Block就会被销毁。例如:
void testStackBlock() {
int num = 10;
void (^stackBlock)(void) = ^{
NSLog(@"Number: %d", num);
};
stackBlock();
}
在testStackBlock
函数中,stackBlock
捕获了局部变量num
,它是一个NSStackBlock
,存储在栈上。当testStackBlock
函数返回后,stackBlock
就会被销毁。
- NSMallocBlock:如果需要延长Block的生命周期,使其在函数返回后仍然可用,我们需要将Block从栈上复制到堆上,即创建一个
NSMallocBlock
。通常在将Block作为属性存储或者传递给其他对象时,需要进行这样的操作。例如:
@interface MyClass : NSObject
@property (nonatomic, copy) void (^heapBlock)(void);
@end
@implementation MyClass
- (void)setUpBlock {
int num = 20;
void (^stackBlock)(void) = ^{
NSLog(@"Number: %d", num);
};
self.heapBlock = [stackBlock copy];
}
@end
在MyClass
类中,setUpBlock
方法中定义了一个stackBlock
,然后通过copy
操作将其复制到堆上,并赋值给heapBlock
属性。这样,heapBlock
的生命周期就与MyClass
对象相关,而不会因为setUpBlock
函数返回而被销毁。
4.2 避免循环引用
在使用Block时,一个常见的问题是循环引用。当Block捕获了包含它的对象(比如self
),而这个对象又持有该Block时,就会形成循环引用,导致内存泄漏。例如:
@interface MyViewController : UIViewController
@property (nonatomic, copy) void (^block)(void);
@end
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.block = ^{
NSLog(@"View controller title: %@", self.title);
};
}
@end
在这个例子中,MyViewController
持有block
属性,而block
又捕获了self
,这就形成了循环引用。为了避免这种情况,我们可以使用弱引用。在ARC环境下,可以使用__weak
关键字。
@interface MyViewController : UIViewController
@property (nonatomic, copy) void (^block)(void);
@end
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"View controller title: %@", strongSelf.title);
}
};
}
@end
这里,我们先创建了一个__weak
类型的weakSelf
引用self
,在Block内部,我们又创建了一个__strong
类型的strongSelf
来临时持有weakSelf
。这样,在Block执行期间,strongSelf
保证MyViewController
对象不会被释放,而weakSelf
不会增加MyViewController
对象的引用计数,从而避免了循环引用。
5. Block与其他回调方式的比较
5.1 与代理模式的比较
- 代码简洁性:
- Block:使用Block进行回调通常代码更加简洁。例如,在处理按钮点击回调时,使用Block只需要在按钮创建时为其设置一个Block属性即可,如前文的
CustomButton
示例。 - 代理模式:使用代理模式需要先定义代理协议,在持有按钮的对象(如视图控制器)中遵循该协议并实现相关方法,代码量相对较多。例如:
- Block:使用Block进行回调通常代码更加简洁。例如,在处理按钮点击回调时,使用Block只需要在按钮创建时为其设置一个Block属性即可,如前文的
// 定义代理协议
@protocol CustomButtonDelegate <NSObject>
- (void)customButtonDidTap:(CustomButton *)button;
@end
// 自定义按钮类
@interface CustomButton : UIButton
@property (nonatomic, weak) id<CustomButtonDelegate> delegate;
@end
@implementation CustomButton
- (void)awakeFromNib {
[super awakeFromNib];
[self addTarget:self action:@selector(handleTap) forControlEvents:UIControlEventTouchUpInside];
}
- (void)handleTap {
if ([self.delegate respondsToSelector:@selector(customButtonDidTap:)]) {
[self.delegate customButtonDidTap:self];
}
}
@end
// 视图控制器遵循代理协议并实现方法
@interface ViewController : UIViewController <CustomButtonDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CustomButton *customButton = [[CustomButton alloc] initWithFrame:CGRectMake(100, 100, 200, 50)];
customButton.delegate = self;
[self.view addSubview:customButton];
}
- (void)customButtonDidTap:(CustomButton *)button {
NSLog(@"Custom button was tapped.");
}
@end
-
灵活性:
- Block:Block可以捕获局部变量,在回调时可以直接使用这些变量,具有更高的灵活性。例如,在网络请求的回调中,Block可以方便地使用请求前定义的局部变量进行数据处理。
- 代理模式:代理模式中,代理方法只能通过参数传递相关信息,灵活性相对较差。如果需要在代理方法中使用多个局部变量,就需要将这些变量作为参数传递,或者在代理对象中定义属性来存储这些变量。
-
可维护性:
- Block:由于代码相对集中,在修改回调逻辑时,只需要修改Block内部的代码,维护起来相对简单。
- 代理模式:代理模式涉及到协议定义、代理设置和代理方法实现等多个部分,如果回调逻辑发生变化,可能需要在多个地方进行修改,维护成本相对较高。
5.2 与通知中心的比较
- 通信范围:
- Block:Block通常用于一对一的通信场景,比如一个视图与持有它的视图控制器之间的交互,或者一个对象与调用它方法的对象之间的回调。
- 通知中心:通知中心用于一对多的通信场景,一个对象发送通知,多个监听该通知的对象都可以收到并进行处理。例如,在一个应用中,当用户登录成功后,可能需要多个不同的模块(如用户信息展示模块、购物车模块等)进行相应的更新,这时使用通知中心就比较合适。
- 耦合度:
- Block:使用Block进行回调,回调的发送方和接收方之间的耦合度相对较高,因为接收方的回调逻辑直接在Block中定义,发送方需要明确知道接收方的回调需求。
- 通知中心:通知中心的耦合度相对较低,发送通知的对象不需要知道哪些对象会接收通知并处理,只需要发送通知即可,接收通知的对象通过注册监听特定通知来处理,它们之间的联系比较松散。
- 性能:
- Block:Block的性能相对较高,因为它是直接的函数调用,没有额外的中间层开销。
- 通知中心:通知中心在发送和接收通知时,需要进行通知的注册、查找和分发等操作,存在一定的性能开销,尤其在大量通知发送和接收的情况下,性能影响可能会更明显。
6. 实际项目中的应用场景扩展
6.1 动画与过渡效果
在iOS应用中,动画和过渡效果是提升用户体验的重要部分。使用Block可以方便地控制动画的开始、结束以及中间过程的回调。例如,在视图过渡动画中:
- (void)performViewTransition {
UIView *fromView = self.view;
UIView *toView = [[UIView alloc] initWithFrame:self.view.bounds];
toView.backgroundColor = [UIColor redColor];
[self.view addSubview:toView];
[UIView transitionFromView:fromView toView:toView duration:0.5 options:UIViewAnimationOptionTransitionCrossDissolve completion:^(BOOL finished) {
if (finished) {
NSLog(@"View transition completed.");
// 可以在这里进行动画完成后的操作,比如移除旧视图
[fromView removeFromSuperview];
}
}];
}
在这个例子中,UIView
的transitionFromView:toView:duration:options:completion:
方法接受一个完成回调Block。当视图过渡动画完成时,该Block会被执行,我们可以在其中进行动画完成后的清理或其他操作,如移除旧视图。通过这种方式,使用Block可以精确控制动画的生命周期和后续操作。
6.2 数据处理流水线
在处理复杂数据流程时,我们可以构建一个数据处理流水线,每个阶段使用Block来封装处理逻辑。例如,在一个图片处理应用中,可能需要依次进行图片加载、图片裁剪、图片滤镜处理和图片保存等操作。
// 图片加载Block
UIImage * (^loadImageBlock)(NSString *imagePath) = ^UIImage*(NSString *imagePath) {
return [UIImage imageWithContentsOfFile:imagePath];
};
// 图片裁剪Block
UIImage * (^cropImageBlock)(UIImage *, CGRect) = ^UIImage*(UIImage *image, CGRect cropRect) {
CGImageRef imageRef = CGImageCreateWithImageInRect([image CGImage], cropRect);
UIImage *croppedImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
return croppedImage;
};
// 图片滤镜处理Block
UIImage * (^applyFilterBlock)(UIImage *) = ^UIImage*(UIImage *image) {
// 这里假设使用Core Image进行滤镜处理,实际代码可能更复杂
CIImage *ciImage = [CIImage imageWithCGImage:image.CGImage];
CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur" keysAndValues:kCIInputImageKey, ciImage, @"inputRadius", @(5.0), nil];
CIImage *resultImage = [filter outputImage];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:resultImage fromRect:resultImage.extent];
UIImage *filteredImage = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return filteredImage;
};
// 图片保存Block
BOOL (^saveImageBlock)(UIImage *, NSString *) = ^BOOL(UIImage *image, NSString *savePath) {
NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
return [imageData writeToFile:savePath atomically:YES];
};
// 构建数据处理流水线
void (^imageProcessingPipeline)(NSString *, CGRect, NSString *) = ^void(NSString *imagePath, CGRect cropRect, NSString *savePath) {
UIImage *loadedImage = loadImageBlock(imagePath);
UIImage *croppedImage = cropImageBlock(loadedImage, cropRect);
UIImage *filteredImage = applyFilterBlock(croppedImage);
BOOL isSaved = saveImageBlock(filteredImage, savePath);
if (isSaved) {
NSLog(@"Image processed and saved successfully.");
} else {
NSLog(@"Image saving failed.");
}
};
// 调用数据处理流水线
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"example" ofType:@"jpg"];
CGRect cropRect = CGRectMake(100, 100, 200, 200);
NSString *savePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"processedImage.jpg"];
imageProcessingPipeline(imagePath, cropRect, savePath);
在这个例子中,我们定义了多个Block分别用于图片加载、裁剪、滤镜处理和保存。然后通过imageProcessingPipeline
Block将这些操作串联起来,形成一个数据处理流水线。这样的结构使得代码逻辑清晰,每个处理阶段的逻辑可以独立修改和扩展,方便维护和优化整个图片处理流程。
6.3 并发编程
在Objective-C中,Grand Central Dispatch(GCD)是常用的并发编程框架,而Block在GCD中起着关键作用。GCD通过队列来管理任务的执行,我们可以将任务封装在Block中并提交到相应的队列。例如,在后台线程执行耗时任务,然后在主线程更新UI:
dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(backgroundQueue, ^{
// 模拟耗时任务,比如计算大量数据
int result = 0;
for (int i = 0; i < 1000000; i++) {
result += i;
}
dispatch_async(mainQueue, ^{
// 在主线程更新UI
self.resultLabel.text = [NSString stringWithFormat:@"Result: %d", result];
});
});
在这个例子中,我们首先获取了一个全局后台队列backgroundQueue
和主线程队列mainQueue
。然后通过dispatch_async
函数将一个包含耗时计算任务的Block提交到后台队列执行。当计算完成后,又通过dispatch_async
将一个更新UI的Block提交到主线程队列执行。通过这种方式,使用Block与GCD结合,我们可以轻松实现多线程并发编程,提高应用的性能和响应性,同时避免在主线程执行耗时任务导致的界面卡顿。
综上所述,在Objective - C编程中,Block在代码封装与回调方面有着强大的功能和广泛的应用场景。通过合理使用Block,我们可以提高代码的可读性、可维护性和性能,构建出更加健壮和高效的应用程序。无论是简单的逻辑封装,还是复杂的业务流程处理,以及异步操作和视图交互等,Block都能为我们提供优雅的解决方案。同时,在使用Block时,我们也需要注意其内存管理和避免循环引用等问题,以确保应用程序的稳定性和可靠性。在实际项目中,根据不同的需求和场景,灵活运用Block与其他编程模式相结合,能够更好地实现项目的功能和目标。