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

Objective-C 中的代理模式详解与应用

2022-06-075.1k 阅读

代理模式的基本概念

在软件开发中,代理模式是一种设计模式,它为其他对象提供一种代理以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介的作用,通过代理对象,客户端可以间接访问目标对象,这样可以在不改变目标对象的基础上,增加一些额外的功能或者对访问进行控制。

从生活中的例子来理解代理模式,比如我们要购买国外的商品,但是可能由于语言、地域等限制,直接购买不方便,这时候我们可以通过代购来帮我们购买商品。代购就相当于代理,我们通过代购间接购买到国外的商品,代购在我们和国外商品之间起到了中介作用,并且代购还可能提供一些额外的服务,比如帮忙挑选商品、负责运输等。

在Objective - C编程中,代理模式同样广泛应用。它提供了一种优雅的方式来实现对象间的通信和功能扩展。当一个对象不想或者不能直接处理某个任务时,可以委托给另一个对象(代理)来处理,这两个对象之间通过遵守特定的协议(protocol)来进行交互。

Objective - C中代理模式的实现要素

  1. 委托方(Delegator):也就是需要借助其他对象完成任务的对象。在Objective - C中,委托方持有代理对象的引用,并在需要的时候调用代理对象的方法。
  2. 代理方(Delegate):接受委托并处理任务的对象。代理方需要遵守委托方定义的协议,实现协议中定义的方法。
  3. 协议(Protocol):定义了委托方和代理方之间交互的接口。协议中声明了一系列方法,代理方需要实现这些方法来完成委托方的任务。

协议的定义与使用

在Objective - C中,协议是定义代理模式交互接口的关键。协议使用@protocol关键字来定义。

@protocol MyDelegateProtocol <NSObject>
- (void)doSomething;
@end

上述代码定义了一个名为MyDelegateProtocol的协议,它继承自NSObject协议(几乎所有的协议都会继承自NSObject协议,这样可以保证协议方法的默认实现和内存管理等功能),并且声明了一个doSomething方法。任何想要成为遵守该协议的代理对象,都需要实现这个方法。

委托方的实现

委托方是发起委托的对象,它需要持有代理对象的引用,并在合适的时机调用代理对象的方法。

#import <Foundation/Foundation.h>
@protocol MyDelegateProtocol <NSObject>
- (void)doSomething;
@end

@interface MyDelegator : NSObject
@property (nonatomic, weak) id<MyDelegateProtocol> delegate;
- (void)startTask;
@end

@implementation MyDelegator
- (void)startTask {
    if ([self.delegate respondsToSelector:@selector(doSomething)]) {
        [self.delegate doSomething];
    }
}
@end

在上述代码中,MyDelegator类就是委托方。它定义了一个delegate属性,类型是id<MyDelegateProtocol>,这表示delegate可以是任何遵守MyDelegateProtocol协议的对象。startTask方法中,首先检查代理对象是否实现了doSomething方法(通过respondsToSelector:方法),如果实现了,则调用代理对象的doSomething方法。

代理方的实现

代理方是实际执行任务的对象,它需要遵守委托方定义的协议,并实现协议中的方法。

#import <Foundation/Foundation.h>
@protocol MyDelegateProtocol <NSObject>
- (void)doSomething;
@end

@interface MyDelegate : NSObject <MyDelegateProtocol>
@end

@implementation MyDelegate
- (void)doSomething {
    NSLog(@"代理对象执行任务");
}
@end

在上述代码中,MyDelegate类遵守了MyDelegateProtocol协议,并实现了doSomething方法。在实际应用中,doSomething方法内可以编写具体的业务逻辑。

代理模式的应用场景

  1. 视图控制器之间的通信:在iOS开发中,视图控制器(ViewController)之间经常需要进行通信。例如,一个视图控制器负责展示列表,另一个视图控制器负责展示详情。当在列表视图控制器中点击某一项时,需要将该项的信息传递给详情视图控制器并展示。这时候可以使用代理模式,列表视图控制器作为委托方,详情视图控制器作为代理方。
// 定义协议
@protocol ItemSelectedDelegate <NSObject>
- (void)itemSelected:(NSString *)item;
@end

// 列表视图控制器(委托方)
@interface ListViewController : UIViewController
@property (nonatomic, weak) id<ItemSelectedDelegate> delegate;
@property (nonatomic, strong) NSArray<NSString *> *items;
@end

@implementation ListViewController
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *selectedItem = self.items[indexPath.row];
    if ([self.delegate respondsToSelector:@selector(itemSelected:)]) {
        [self.delegate itemSelected:selectedItem];
    }
}
@end

// 详情视图控制器(代理方)
@interface DetailViewController : UIViewController <ItemSelectedDelegate>
@end

@implementation DetailViewController
- (void)itemSelected:(NSString *)item {
    self.title = item;
    // 这里可以根据item加载详情数据并展示
}
@end

在上述代码中,当用户在ListViewController的表格视图中点击某一行时,会调用代理对象的itemSelected:方法,DetailViewController作为代理方实现这个方法来展示详情。

  1. 异步任务的回调处理:在进行网络请求等异步任务时,我们通常希望在任务完成后得到通知并处理结果。代理模式可以很好地实现这一点。
// 定义协议
@protocol NetworkRequestDelegate <NSObject>
- (void)requestFinishedWithData:(NSData *)data;
- (void)requestFailedWithError:(NSError *)error;
@end

// 网络请求类(委托方)
@interface NetworkRequest : NSObject
@property (nonatomic, weak) id<NetworkRequestDelegate> delegate;
- (void)startRequest;
@end

@implementation NetworkRequest
- (void)startRequest {
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"http://example.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            if ([self.delegate respondsToSelector:@selector(requestFailedWithError:)]) {
                [self.delegate requestFailedWithError:error];
            }
        } else {
            if ([self.delegate respondsToSelector:@selector(requestFinishedWithData:)]) {
                [self.delegate requestFinishedWithData:data];
            }
        }
    }];
    [task resume];
}
@end

// 代理方
@interface ViewController : UIViewController <NetworkRequestDelegate>
@property (nonatomic, strong) NetworkRequest *request;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.request = [[NetworkRequest alloc] init];
    self.request.delegate = self;
    [self.request startRequest];
}

- (void)requestFinishedWithData:(NSData *)data {
    NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"请求成功,结果:%@", result);
}

- (void)requestFailedWithError:(NSError *)error {
    NSLog(@"请求失败,错误:%@", error);
}
@end

在上述代码中,NetworkRequest类负责发起网络请求,在请求完成后,根据结果调用代理对象的不同方法。ViewController作为代理方,实现这些方法来处理请求成功或失败的情况。

  1. 控件事件的处理:在iOS开发中,很多控件都使用代理模式来处理事件。例如,UITableView的数据源和代理就是通过协议来实现的。UITableView作为委托方,视图控制器作为代理方,视图控制器通过实现UITableViewDataSourceUITableViewDelegate协议中的方法来提供表格的数据和处理表格的交互事件。
@interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray<NSString *> *dataSource;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.dataSource = @[@"Item 1", @"Item 2", @"Item 3"];
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.view addSubview:self.tableView];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataSource.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    cell.textLabel.text = self.dataSource[indexPath.row];
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"点击了第%ld行", (long)indexPath.row);
}
@end

在上述代码中,ViewController遵守了UITableViewDataSourceUITableViewDelegate协议,实现了协议中的方法,为UITableView提供数据和处理点击事件。

代理模式的优点

  1. 解耦对象之间的关系:委托方和代理方之间通过协议进行交互,它们不需要了解对方的具体实现细节,只需要关注协议中定义的接口。这样可以降低对象之间的耦合度,使得代码更加灵活和可维护。例如,在视图控制器之间的通信场景中,列表视图控制器不需要知道详情视图控制器是如何展示详情的,只需要按照协议调用代理方法传递数据即可。
  2. 提高代码的可扩展性:当需要增加新的功能时,只需要在代理对象中实现协议方法即可,而不需要修改委托方的代码。例如,在异步任务回调处理中,如果需要增加新的处理逻辑,只需要在代理方的相应方法中添加代码,而不会影响到网络请求类(委托方)。
  3. 实现代码复用:同一个代理对象可以被多个委托方使用,只要这些委托方遵守相同的协议。例如,在多个不同的视图控制器中进行网络请求时,可以使用同一个代理对象来处理请求结果,提高了代码的复用性。

代理模式的缺点

  1. 增加了代码的复杂性:虽然代理模式降低了对象之间的耦合度,但同时也引入了协议、委托方和代理方等多个概念,增加了代码的复杂度。特别是在大型项目中,可能会有大量的协议和代理关系,需要花费更多的精力来管理和维护。
  2. 可能导致性能问题:在代理模式中,每次调用代理方法都需要进行方法查找和消息转发,这在一定程度上会影响性能。特别是在频繁调用代理方法的情况下,性能问题可能会更加明显。不过,在现代的Objective - C运行时环境下,这种性能开销通常是可以接受的。

代理模式与其他设计模式的比较

  1. 与观察者模式的比较:观察者模式也是一种用于对象间通信的设计模式。它主要用于一对多的通知场景,即当一个对象状态发生变化时,会通知所有依赖它的对象。而代理模式主要用于一对一的委托场景,代理对象代表委托对象执行任务。例如,在一个天气应用中,当天气数据更新时,多个视图(如主界面、通知中心等)都需要更新,这时候适合使用观察者模式。而如果一个视图控制器需要另一个视图控制器帮忙处理某个特定任务,就适合使用代理模式。
  2. 与装饰器模式的比较:装饰器模式主要用于动态地给对象添加新的功能,它通过组合的方式将对象包装起来,在不改变对象原有结构的基础上增加功能。代理模式虽然也可以在一定程度上增加功能,但它的重点是控制对对象的访问。例如,要给一个文本视图添加加粗、变色等功能,可以使用装饰器模式。而如果要控制对一个网络请求对象的访问,确保请求在特定条件下才执行,就可以使用代理模式。

代理模式在iOS框架中的应用实例

  1. UIScrollView的代理UIScrollView是iOS开发中常用的用于滚动内容的视图。它通过代理模式来处理滚动相关的事件。UIScrollView的代理需要遵守UIScrollViewDelegate协议,该协议定义了一系列方法,如scrollViewDidScroll:用于在滚动视图滚动时调用,scrollViewWillBeginDragging:用于在开始拖动滚动视图时调用等。
@interface ViewController : UIViewController <UIScrollViewDelegate>
@property (nonatomic, strong) UIScrollView *scrollView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.scrollView.contentSize = CGSizeMake(self.view.bounds.size.width * 3, self.view.bounds.size.height);
    self.scrollView.delegate = self;
    [self.view addSubview:self.scrollView];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    NSLog(@"滚动视图滚动了,当前偏移量:%@", NSStringFromCGPoint(scrollView.contentOffset));
}
@end

在上述代码中,ViewController作为UIScrollView的代理,实现了scrollViewDidScroll:方法来处理滚动事件。 2. AVPlayer的代理AVPlayer是iOS中用于播放音频和视频的类。它通过代理模式来处理播放相关的事件。AVPlayer的代理需要遵守AVPlayerItemDelegate协议,例如playerItemDidPlayToEndTime:方法用于在播放到媒体文件末尾时调用。

#import <AVFoundation/AVFoundation.h>

@interface ViewController : UIViewController <AVPlayerItemDelegate>
@property (nonatomic, strong) AVPlayer *player;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSURL *url = [NSURL URLWithString:@"http://example.com/video.mp4"];
    AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
    self.player = [AVPlayer playerWithPlayerItem:item];
    item.delegate = self;
    [self.player play];
}

- (void)playerItemDidPlayToEndTime:(AVPlayerItem *)playerItem {
    NSLog(@"视频播放结束");
}
@end

在上述代码中,ViewController作为AVPlayerItem的代理,实现了playerItemDidPlayToEndTime:方法来处理视频播放结束的事件。

正确使用代理模式的注意事项

  1. 避免循环引用:在设置代理关系时,要注意避免循环引用。如果委托方和代理方相互持有对方的强引用,就会导致循环引用,从而造成内存泄漏。通常,委托方对代理方的引用使用weakassign修饰符(在ARC环境下建议使用weak,在MRC环境下建议使用assign)。例如,在前面的MyDelegator类中,delegate属性使用了weak修饰符,这样可以避免循环引用。
  2. 合理定义协议方法:协议方法应该根据实际需求进行合理定义。方法的参数和返回值应该能够满足委托方和代理方之间的交互需求。同时,协议方法的命名应该清晰明了,便于理解和使用。例如,在ItemSelectedDelegate协议中,itemSelected:方法的命名就很直观地表明了该方法的作用是传递选中的项目。
  3. 检查代理是否实现方法:在委托方调用代理方法之前,一定要检查代理对象是否实现了该方法,通过respondsToSelector:方法进行检查。这样可以避免因代理对象未实现方法而导致的程序崩溃。例如,在MyDelegator类的startTask方法中,就先检查了代理对象是否实现了doSomething方法。

代理模式在实际项目中的优化

  1. 使用块(Block)作为代理的替代方案:在某些情况下,块可以作为代理模式的一种替代方案。块可以更简洁地实现一些简单的委托逻辑,并且避免了协议定义和代理对象实现的繁琐过程。例如,在进行简单的网络请求回调处理时,可以使用块来代替代理。
@interface NetworkRequest : NSObject
- (void)startRequestWithCompletion:(void(^)(NSData *data, NSError *error))completion;
@end

@implementation NetworkRequest
- (void)startRequestWithCompletion:(void(^)(NSData *data, NSError *error))completion {
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"http://example.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (completion) {
            completion(data, error);
        }
    }];
    [task resume];
}
@end

@interface ViewController : UIViewController
@property (nonatomic, strong) NetworkRequest *request;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.request = [[NetworkRequest alloc] init];
    [self.request startRequestWithCompletion:^(NSData *data, NSError *error) {
        if (error) {
            NSLog(@"请求失败,错误:%@", error);
        } else {
            NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSLog(@"请求成功,结果:%@", result);
        }
    }];
}
@end

在上述代码中,NetworkRequest类使用块来处理网络请求的回调,ViewController在调用网络请求时直接传入块来处理结果,代码更加简洁。 2. 协议方法的默认实现:对于一些常用的协议方法,可以在协议定义时提供默认实现。这样,代理对象如果不需要特殊处理,可以不实现这些方法,减少了代理对象的代码量。例如,在自定义的协议中,可以使用@optional关键字来声明可选方法,并在协议扩展中提供默认实现。

@protocol MyCustomProtocol <NSObject>
@optional
- (void)defaultMethod;
@end

@interface MyCustomProtocol (DefaultImplementation)
- (void)defaultMethod {
    NSLog(@"这是默认实现");
}
@end

@interface MyDelegate : NSObject <MyCustomProtocol>
@end

@implementation MyDelegate
// 这里可以不实现defaultMethod,使用默认实现
@end

在上述代码中,MyCustomProtocol协议定义了一个可选方法defaultMethod,并在协议扩展中提供了默认实现。MyDelegate类作为代理对象可以选择不实现该方法,直接使用默认实现。

通过以上对Objective - C中代理模式的详细讲解、代码示例以及应用场景、优缺点等方面的分析,相信开发者对代理模式在Objective - C编程中的应用有了更深入的理解,能够在实际项目中更加灵活、准确地运用代理模式来优化代码结构和实现功能。