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

Objective-C中的协议(Protocol)与代理模式实践

2021-10-013.9k 阅读

协议(Protocol)的基础概念

协议的定义与作用

在Objective-C中,协议(Protocol)是一种特殊的接口定义方式。它定义了一组方法列表,但并不提供这些方法的实现。协议的主要作用在于允许不同类的对象采用一组标准的行为方式。通过协议,一个类可以声明它能够执行某些特定的任务,即使这些类之间可能并没有直接的继承关系。

例如,在一个图形绘制的应用中,可能有圆形、矩形、三角形等不同的图形类。如果我们想要实现一个功能,让这些不同的图形类都能实现“绘制自身”和“计算面积”的操作,由于这些图形类可能并没有共同的父类(除了根类NSObject,但NSObject不会提供这些特定图形的操作实现),这时就可以使用协议来定义“绘制”和“计算面积”的方法,然后让各个图形类遵循这个协议并实现这些方法。

协议的定义语法

定义一个协议使用@protocol关键字,语法如下:

@protocol ProtocolName <NSObject>
// 方法声明列表
- (void)method1;
- (int)method2WithParameter:(int)param;
@end

在上述代码中,@protocol ProtocolName <NSObject>表示定义一个名为ProtocolName的协议,尖括号中的NSObject表示这个协议继承自NSObject协议(NSObject协议定义了一些基础的方法,如description等,几乎所有的Objective-C类都遵循NSObject协议)。协议中可以声明实例方法(如method1method2WithParameter:),方法声明的语法与类中方法声明基本相同,但协议中只声明方法,不提供实现。

协议中的方法类型

  1. 必需方法(Required Methods):默认情况下,协议中声明的方法都是必需方法。当一个类遵循某个协议时,必须实现该协议中的所有必需方法,否则编译器会发出警告。例如:
@protocol RequiredProtocol <NSObject>
- (void)requiredMethod;
@end

@interface MyClass : NSObject <RequiredProtocol>
@end

@implementation MyClass
// 必须实现requiredMethod,否则编译器报错
- (void)requiredMethod {
    NSLog(@"This is the implementation of requiredMethod.");
}
@end
  1. 可选方法(Optional Methods):如果希望协议中的某些方法是可选的,让遵循协议的类可以选择是否实现这些方法,可以使用@optional关键字。例如:
@protocol OptionalProtocol <NSObject>
@required
- (void)requiredMethod;
@optional
- (void)optionalMethod;
@end

@interface AnotherClass : NSObject <OptionalProtocol>
@end

@implementation AnotherClass
- (void)requiredMethod {
    NSLog(@"Implementing required method.");
}
// 可以选择不实现optionalMethod
@end

当调用遵循协议的对象的可选方法时,需要先检查对象是否响应该方法,以避免运行时错误。可以使用respondsToSelector:方法来检查,例如:

AnotherClass *obj = [[AnotherClass alloc] init];
if ([obj respondsToSelector:@selector(optionalMethod)]) {
    [obj optionalMethod];
}

遵循协议

类遵循协议的方式

一个类遵循协议非常简单,只需在类的声明中,在类名之后加上<ProtocolName>,其中ProtocolName是要遵循的协议名称。例如:

@protocol SampleProtocol <NSObject>
- (void)sampleMethod;
@end

@interface MyClass : NSObject <SampleProtocol>
@end

@implementation MyClass
- (void)sampleMethod {
    NSLog(@"This is the implementation of sampleMethod.");
}
@end

在上述代码中,MyClass类通过<SampleProtocol>声明它遵循SampleProtocol协议,并实现了sampleMethod方法。

父类与子类遵循协议的关系

如果一个父类遵循了某个协议,子类会自动继承父类对该协议的遵循。例如:

@protocol ParentProtocol <NSObject>
- (void)parentProtocolMethod;
@end

@interface ParentClass : NSObject <ParentProtocol>
@end

@implementation ParentClass
- (void)parentProtocolMethod {
    NSLog(@"Parent class implementation of parentProtocolMethod.");
}
@end

@interface ChildClass : ParentClass
@end

// ChildClass自动遵循ParentProtocol,并且继承了父类对parentProtocolMethod的实现
// 当然,ChildClass也可以重写该方法

如果子类想要对协议方法提供不同的实现,可以在子类中重写该方法。例如:

@implementation ChildClass
- (void)parentProtocolMethod {
    NSLog(@"Child class implementation of parentProtocolMethod.");
}
@end

多个协议的遵循

一个类可以同时遵循多个协议,只需在类声明中用逗号分隔协议名称即可。例如:

@protocol ProtocolA <NSObject>
- (void)methodA;
@end

@protocol ProtocolB <NSObject>
- (void)methodB;
@end

@interface MyMultiProtocolClass : NSObject <ProtocolA, ProtocolB>
@end

@implementation MyMultiProtocolClass
- (void)methodA {
    NSLog(@"Implementation of methodA from ProtocolA.");
}

- (void)methodB {
    NSLog(@"Implementation of methodB from ProtocolB.");
}
@end

在上述代码中,MyMultiProtocolClass类同时遵循了ProtocolAProtocolB协议,并实现了这两个协议中的方法。

协议与代理模式

代理模式概述

代理模式是一种设计模式,在这种模式中,一个对象(代理对象)代表另一个对象(被代理对象)来执行某些操作。代理对象接收请求,并将请求转发给被代理对象,同时可以在转发前后执行一些额外的逻辑,如权限检查、缓存处理等。

在Objective-C中,协议与代理模式紧密结合。通常,会定义一个协议来声明代理对象需要实现的方法,被代理对象持有一个遵循该协议的代理对象的引用。当被代理对象发生某些事件时,它会调用代理对象的相应方法,通知代理对象并让代理对象来处理相关逻辑。

代理模式的实现步骤

  1. 定义协议:首先定义一个协议,声明代理对象需要实现的方法。例如,假设我们有一个Downloader类负责下载文件,当下载完成时需要通知代理对象。可以定义如下协议:
@protocol DownloaderDelegate <NSObject>
@optional
- (void)downloadDidFinish:(NSString *)fileName;
- (void)downloadDidFailWithError:(NSError *)error;
@end
  1. 在被代理对象中声明代理属性:在Downloader类中声明一个代理属性,并遵循NSObject协议(因为代理协议继承自NSObject协议)。
@interface Downloader : NSObject
@property (nonatomic, weak) id<DownloaderDelegate> delegate;
- (void)startDownloading:(NSString *)fileName;
@end

这里使用weak修饰符来避免循环引用(如果使用strong修饰符,代理对象持有被代理对象,被代理对象又持有代理对象,会导致循环引用,对象无法正常释放)。

  1. 实现被代理对象的逻辑并调用代理方法:在Downloader类的实现中,当下载完成或失败时调用代理方法。
@implementation Downloader
- (void)startDownloading:(NSString *)fileName {
    // 模拟下载逻辑
    BOOL success = arc4random_uniform(2); // 随机模拟下载成功或失败
    if (success) {
        if ([self.delegate respondsToSelector:@selector(downloadDidFinish:)]) {
            [self.delegate downloadDidFinish:fileName];
        }
    } else {
        NSError *error = [NSError errorWithDomain:@"DownloadError" code:1 userInfo:nil];
        if ([self.delegate respondsToSelector:@selector(downloadDidFailWithError:)]) {
            [self.delegate downloadDidFailWithError:error];
        }
    }
}
@end
  1. 创建代理对象并设置代理:在使用Downloader类的地方,创建一个代理对象并将其设置为Downloader对象的代理。
@interface DownloaderDelegateObject : NSObject <DownloaderDelegate>
@end

@implementation DownloaderDelegateObject
- (void)downloadDidFinish:(NSString *)fileName {
    NSLog(@"Download of %@ finished successfully.", fileName);
}

- (void)downloadDidFailWithError:(NSError *)error {
    NSLog(@"Download failed with error: %@", error);
}
@end

// 使用
Downloader *downloader = [[Downloader alloc] init];
DownloaderDelegateObject *delegate = [[DownloaderDelegateObject alloc] init];
downloader.delegate = delegate;
[downloader startDownloading:@"exampleFile.txt"];

在上述代码中,DownloaderDelegateObject类遵循DownloaderDelegate协议并实现了相关方法。Downloader类在下载完成或失败时,通过代理对象调用相应的方法,实现了代理模式。

代理模式的优点

  1. 解耦:被代理对象和代理对象之间通过协议进行交互,它们之间的耦合度较低。被代理对象只关心代理对象是否遵循协议并实现了相关方法,而不关心代理对象的具体类型。这样,当需要更换代理对象的实现时,只需要创建一个新的遵循相同协议的类,而不需要修改被代理对象的代码。
  2. 灵活性:代理模式使得代码的扩展性更强。例如,如果需要在下载完成后执行不同的操作,可以创建不同的代理对象,每个代理对象实现不同的downloadDidFinish:方法逻辑。同时,协议中的可选方法也提供了更多的灵活性,代理对象可以根据实际需求选择实现哪些方法。
  3. 可维护性:将不同的功能逻辑分离到被代理对象和代理对象中,使得代码结构更加清晰,易于维护。例如,下载逻辑在Downloader类中,而下载完成后的处理逻辑在代理对象中,当需要修改下载完成后的处理逻辑时,只需要修改代理对象的代码,而不会影响到下载逻辑。

协议的继承与组合

协议的继承

协议可以继承其他协议,就像类继承父类一样。通过协议继承,可以创建一个新的协议,它包含了父协议的所有方法声明,同时还可以添加自己的方法声明。语法如下:

@protocol ParentProtocol <NSObject>
- (void)parentMethod;
@end

@protocol ChildProtocol : ParentProtocol
- (void)childMethod;
@end

在上述代码中,ChildProtocol协议继承自ParentProtocol协议。一个遵循ChildProtocol协议的类,不仅需要实现childMethod方法,还需要实现parentMethod方法(如果parentMethod是必需方法)。

协议的组合

有时候,可能需要创建一个新的协议,它包含多个现有协议的方法。虽然协议不能像类那样多重继承,但可以通过组合来实现类似的效果。例如,假设有ProtocolAProtocolB两个协议:

@protocol ProtocolA <NSObject>
- (void)methodA;
@end

@protocol ProtocolB <NSObject>
- (void)methodB;
@end

可以创建一个新的协议CombinedProtocol,它包含ProtocolAProtocolB的方法:

@protocol CombinedProtocol <ProtocolA, ProtocolB>
// 也可以添加自己的方法声明
- (void)combinedMethod;
@end

一个遵循CombinedProtocol协议的类,需要实现methodAmethodBcombinedMethod方法(假设它们都是必需方法)。

协议继承与组合的应用场景

  1. 代码复用:通过协议继承,可以避免在多个协议中重复声明相同的方法。例如,如果多个协议都需要一些基础的方法,将这些基础方法放在一个父协议中,然后让其他协议继承这个父协议,这样可以提高代码的复用性。
  2. 功能组合:协议组合适用于将多个不同功能的协议组合成一个新的协议,以满足特定的需求。例如,在一个多媒体应用中,可能有音频播放协议、视频播放协议和文件管理协议。通过组合这些协议,可以创建一个多媒体操作协议,让一个类遵循这个协议就可以实现音频、视频播放以及相关文件管理的功能。

协议在实际项目中的应用案例

视图控制器间的通信

在iOS开发中,视图控制器(UIViewController)之间经常需要进行通信。例如,一个列表视图控制器(ListViewController)展示了一些数据项,当用户点击某个数据项时,需要跳转到详情视图控制器(DetailViewController)并传递相关数据。同时,当详情视图控制器中的数据发生变化时,可能需要通知列表视图控制器进行更新。这时候就可以使用协议和代理模式来实现。

首先,定义一个协议:

@protocol DetailViewControllerDelegate <NSObject>
- (void)detailViewControllerDidUpdateData:(id)data;
@end

然后,在DetailViewController中声明代理属性:

@interface DetailViewController : UIViewController
@property (nonatomic, weak) id<DetailViewControllerDelegate> delegate;
@end

DetailViewController的实现中,当数据发生变化时调用代理方法:

@implementation DetailViewController
- (IBAction)saveButtonTapped:(id)sender {
    id updatedData = // 获取更新后的数据
    if ([self.delegate respondsToSelector:@selector(detailViewControllerDidUpdateData:)]) {
        [self.delegate detailViewControllerDidUpdateData:updatedData];
    }
    [self.navigationController popViewControllerAnimated:YES];
}
@end

ListViewController中,创建DetailViewController并设置代理:

@interface ListViewController : UIViewController <DetailViewControllerDelegate>
@property (nonatomic, strong) NSMutableArray *dataArray;
@end

@implementation ListViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"ShowDetail"]) {
        DetailViewController *detailVC = segue.destinationViewController;
        detailVC.delegate = self;
    }
}

- (void)detailViewControllerDidUpdateData:(id)data {
    // 更新列表数据
    [self.dataArray addObject:data];
    [self.tableView reloadData];
}
@end

通过这种方式,实现了视图控制器之间的解耦通信,提高了代码的可维护性和扩展性。

数据加载与处理

在一个网络请求数据加载的场景中,可能有多个视图需要展示相同类型的数据。可以使用协议和代理模式来实现数据的加载和分发。

定义一个数据加载协议:

@protocol DataLoaderDelegate <NSObject>
@optional
- (void)dataLoaderDidFinishLoading:(NSArray *)data;
- (void)dataLoaderDidFailWithError:(NSError *)error;
@end

创建一个数据加载类:

@interface DataLoader : NSObject
@property (nonatomic, weak) id<DataLoaderDelegate> delegate;
- (void)loadData;
@end

@implementation DataLoader
- (void)loadData {
    // 模拟网络请求
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"http://example.com/api/data"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (!error) {
            NSArray *loadedData = // 解析数据
            if ([self.delegate respondsToSelector:@selector(dataLoaderDidFinishLoading:)]) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.delegate dataLoaderDidFinishLoading:loadedData];
                });
            }
        } else {
            if ([self.delegate respondsToSelector:@selector(dataLoaderDidFailWithError:)]) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.delegate dataLoaderDidFailWithError:error];
                });
            }
        }
    }];
    [task resume];
}
@end

在各个视图控制器中遵循协议并设置代理:

@interface FirstViewController : UIViewController <DataLoaderDelegate>
@property (nonatomic, strong) DataLoader *dataLoader;
@property (nonatomic, strong) NSMutableArray *dataArray;
@end

@implementation FirstViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.dataLoader = [[DataLoader alloc] init];
    self.dataLoader.delegate = self;
    [self.dataLoader loadData];
}

- (void)dataLoaderDidFinishLoading:(NSArray *)data {
    self.dataArray = [NSMutableArray arrayWithArray:data];
    [self.tableView reloadData];
}

- (void)dataLoaderDidFailWithError:(NSError *)error {
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
    [alert addAction:okAction];
    [self presentViewController:alert animated:YES completion:nil];
}
@end

同样,其他视图控制器也可以遵循DataLoaderDelegate协议并设置代理,从而实现多个视图共享数据加载逻辑,并且在数据加载完成或失败时进行相应的处理。

协议使用中的注意事项

避免循环引用

在使用代理模式时,如前文所述,一定要注意避免循环引用。如果被代理对象对代理对象使用strong修饰符,而代理对象又持有对被代理对象的强引用(例如代理对象需要调用被代理对象的某些方法,从而持有被代理对象的引用),就会形成循环引用,导致对象无法正常释放,造成内存泄漏。因此,通常在被代理对象中对代理属性使用weak修饰符(如果代理对象的生命周期至少和被代理对象一样长),或者使用unsafe_unretained修饰符(注意unsafe_unretained不会自动将指针设为nil,当对象释放后,指针会成为野指针,使用时需要小心)。

协议方法的命名规范

协议方法的命名应该清晰明了,能够准确表达方法的功能。遵循Objective-C的命名规范,方法名应该以动词开头,并且尽量详细。例如,downloadDidFinish:finish:更能清晰地表达这是下载完成的通知方法。同时,方法名应该避免与其他常用框架或类中的方法名冲突,以减少命名空间的混乱。

协议的版本管理

随着项目的发展,可能需要对协议进行更新,添加新的方法或修改现有方法的参数。在这种情况下,需要谨慎处理,以确保现有遵循该协议的类仍然能够正常工作。一种常见的做法是添加新方法时将其声明为可选方法,这样现有的类不需要立即实现这些新方法。如果需要修改现有必需方法的参数,可能需要考虑提供一个兼容旧版本的过渡方法,同时逐步引导开发者更新代码以使用新的方法。

通过深入理解Objective-C中的协议与代理模式,并在实际项目中合理应用,能够提高代码的可维护性、可扩展性和灵活性,使代码结构更加清晰,更易于团队协作开发。在实际编程过程中,不断积累经验,熟练掌握协议与代理模式的各种技巧和注意事项,能够更好地应对各种复杂的需求场景。例如,在大型项目中,多个模块之间通过协议和代理进行通信和协作,能够有效地降低模块之间的耦合度,提高整个项目的架构质量。同时,对于一些开源项目或框架的使用,理解其内部的协议和代理机制,也有助于开发者更好地进行定制和扩展,以满足自身项目的特殊需求。总之,协议与代理模式是Objective-C编程中非常重要的组成部分,值得开发者深入学习和研究。