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

深入解析Objective-C协议与委托在运行时的工作方式

2021-05-134.7k 阅读

Objective-C协议基础

协议的定义与声明

在Objective-C中,协议(Protocol)是一种非常强大的特性,它定义了一组方法的声明,但并不包含这些方法的实现。协议的主要作用是实现类似多重继承的功能,使得一个类可以遵循多个协议,从而具备多种不同的行为。

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

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

在上述代码中,@protocol ProtocolName <NSObject>表示定义了一个名为ProtocolName的协议,并且该协议继承自NSObject协议。NSObject协议定义了一些基础的方法,几乎所有的Objective-C类都遵循这个协议。协议中可以声明实例方法(如method1method2WithParameter:),也可以声明类方法(使用+号开头)。

类遵循协议

当一个类需要遵循某个协议时,在类的定义中使用尖括号(<>)将协议名括起来,例如:

@interface MyClass : NSObject <ProtocolName>
// 类的其他声明
@end

这里MyClass类声明遵循了ProtocolName协议。这意味着MyClass类必须实现ProtocolName协议中声明的所有方法(除非这些方法被标记为可选的,后面会介绍)。

可选方法与必需方法

在协议中,方法默认是必需的,即遵循该协议的类必须实现这些方法。但可以通过@optional@required关键字来指定方法的可选性。

@protocol AnotherProtocol <NSObject>
@required
- (void)requiredMethod;
@optional
- (void)optionalMethod;
@end

在上述协议AnotherProtocol中,requiredMethod是必需方法,遵循该协议的类必须实现;而optionalMethod是可选方法,遵循该协议的类可以选择实现或不实现。

委托模式与协议的结合

委托模式简介

委托(Delegate)模式是一种设计模式,在Objective-C中经常使用协议来实现委托模式。委托模式的核心思想是将某个对象的部分功能委托给另一个对象来处理。例如,一个视图控制器(ViewController)可能将处理用户在某个视图上的复杂操作的任务委托给另一个专门的对象。

委托协议的定义

通常,委托协议会定义一系列方法,这些方法用于通知委托对象某个事件的发生。以一个简单的按钮点击委托为例,定义如下协议:

@protocol ButtonDelegate <NSObject>
@optional
- (void)buttonDidClick:(UIButton *)button;
@end

这个协议ButtonDelegate定义了一个可选方法buttonDidClick:,当按钮被点击时,拥有该按钮的对象可能会调用这个方法通知其委托对象。

委托的设置与使用

假设我们有一个自定义的MyButton类,它需要一个委托来处理点击事件:

@interface MyButton : UIButton
@property (nonatomic, weak) id<ButtonDelegate> delegate;
@end

@implementation MyButton
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents {
    if (controlEvents & UIControlEventTouchUpInside) {
        if ([self.delegate respondsToSelector:@selector(buttonDidClick:)]) {
            [self.delegate buttonDidClick:self];
        }
    }
    [super sendActionsForControlEvents:controlEvents];
}
@end

MyButton类中,我们定义了一个delegate属性,类型为id<ButtonDelegate>,表示任何遵循ButtonDelegate协议的对象都可以作为它的委托。在sendActionsForControlEvents:方法中,当按钮被点击(UIControlEventTouchUpInside事件)时,会检查委托是否实现了buttonDidClick:方法,如果实现了则调用该方法。

在视图控制器中使用这个MyButton并设置委托:

@interface ViewController : UIViewController <ButtonDelegate>
@property (nonatomic, strong) MyButton *myButton;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.myButton = [MyButton buttonWithType:UIButtonTypeSystem];
    self.myButton.frame = CGRectMake(100, 100, 200, 50);
    [self.myButton setTitle:@"点击我" forState:UIControlStateNormal];
    self.myButton.delegate = self;
    [self.view addSubview:self.myButton];
}

- (void)buttonDidClick:(UIButton *)button {
    NSLog(@"按钮被点击了");
}
@end

ViewController中,我们让它遵循ButtonDelegate协议,并实现了buttonDidClick:方法。在viewDidLoad方法中,创建了MyButton并将ViewController自身设置为MyButton的委托。

Objective-C运行时基础

运行时概述

Objective-C是一种动态语言,其很多特性在运行时(Runtime)才会真正起作用。运行时系统是Objective-C的核心,它负责处理对象的创建、方法的查找与调用等重要操作。运行时的主要功能包括:

  1. 对象的创建与内存管理:运行时系统负责分配和释放对象的内存空间,并且在对象创建时会初始化其成员变量。
  2. 方法的动态绑定:在编译时,Objective-C编译器并不知道对象真正会调用哪个方法的实现,而是在运行时根据对象的类型来查找并调用合适的方法。

运行时相关的头文件与数据结构

运行时相关的功能主要定义在<objc/runtime.h>头文件中。其中一些重要的数据结构包括:

  1. objc_class结构体:用于描述类的信息,包含类名、父类、实例变量列表、方法列表等。
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    #if!defined(__OBJC2__)
    Class _Nullable super_class                                        OBJC2_UNAVAILABLE;
    const char * _Nonnull name                                         OBJC2_UNAVAILABLE;
    long version                                                        OBJC2_UNAVAILABLE;
    long info                                                           OBJC2_UNAVAILABLE;
    long instance_size                                                  OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists          OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                                   OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols                     OBJC2_UNAVAILABLE;
    #endif
};
  1. objc_method结构体:用于描述方法的信息,包括方法名、方法类型编码和方法实现的函数指针。
struct objc_method {
    SEL _Nonnull method_name                                          OBJC2_UNAVAILABLE;
    char * _Nullable method_types                                       OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                            OBJC2_UNAVAILABLE;
};
  1. SEL类型:方法选择器(Selector),本质上是一个指向方法名的字符串的指针。在运行时,通过SEL来查找方法的实现。

运行时方法调用流程

当一个对象接收到一个消息(方法调用)时,运行时系统会按照以下步骤进行处理:

  1. isa指针查找类:首先,通过对象的isa指针找到对象所属的类。
  2. 方法缓存查找:在类的方法缓存(cache)中查找是否有对应的方法实现。如果找到了,直接调用该方法实现,这一步可以提高方法调用的效率。
  3. 方法列表查找:如果在方法缓存中没有找到,会在类的方法列表(methodLists)中查找。如果找到了,将方法实现添加到方法缓存中,然后调用该方法实现。
  4. 父类查找:如果在本类的方法列表中没有找到,会沿着继承链在父类的方法列表中查找,重复上述方法缓存查找和方法列表查找的过程,直到找到方法实现或者到达根类NSObject
  5. 动态方法解析:如果最终没有找到方法实现,运行时系统会进入动态方法解析阶段。可以通过+ (BOOL)resolveInstanceMethod:(SEL)sel(实例方法)或+ (BOOL)resolveClassMethod:(SEL)sel(类方法)方法来动态添加方法的实现。
  6. 备用接收者:如果动态方法解析没有处理该方法,运行时系统会尝试寻找备用接收者,即调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,看是否有其他对象可以处理该消息。
  7. 完整的消息转发:如果仍然没有找到处理该消息的对象,运行时系统会进入完整的消息转发阶段,包括- (void)forwardInvocation:(NSInvocation *)anInvocation+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法的调用,在这个阶段可以更灵活地处理未找到实现的方法调用。

协议在运行时的工作方式

协议的存储与查找

在运行时,每个类的objc_class结构体中的protocols字段指向一个objc_protocol_list结构体,这个结构体包含了该类所遵循的所有协议的列表。

struct objc_protocol_list {
    struct objc_protocol_list * _Nullable next;
    long count;
    __unsafe_unretained Protocol * _Nonnull list[0];
};

当需要检查一个类是否遵循某个协议时,运行时系统会遍历这个协议列表。例如,respondsToSelector:方法在判断一个对象是否能响应某个选择器时,如果该选择器对应的方法是在协议中声明的,就会通过这种方式检查对象所属的类是否遵循包含该方法的协议。

协议方法的查找与调用

当一个对象调用一个在协议中声明的方法时,运行时系统首先会按照常规的方法查找流程(方法缓存、方法列表、父类查找等)在该对象所属的类及其继承链中查找方法实现。如果在类的方法列表和继承链中都没有找到该方法的实现,并且该方法是协议中的必需方法,那么就会产生编译警告(如果是可选方法则不会有编译警告)。

在运行时,如果没有找到方法实现,会进入动态方法解析和消息转发等流程,和普通方法的处理流程类似。例如,假设MyClass类遵循了ProtocolName协议,当MyClass的实例调用ProtocolName协议中声明的method1方法时:

MyClass *obj = [[MyClass alloc] init];
[obj method1];

运行时系统先通过objisa指针找到MyClass类,然后在MyClass类的方法缓存和方法列表中查找method1方法的实现。如果没有找到,会在其父类中查找。如果最终没有找到,会按照动态方法解析和消息转发的流程处理。

委托在运行时的工作方式

委托关系的建立与维护

在运行时,委托关系是通过对象的属性来建立和维护的。以之前的MyButtonViewController为例,在ViewControllerviewDidLoad方法中:

self.myButton.delegate = self;

这行代码将ViewController对象赋值给MyButtondelegate属性,从而建立了委托关系。在运行时,MyButton对象通过这个delegate属性来保存对ViewController对象的引用(这里使用weak修饰符是为了避免循环引用)。

委托方法的调用过程

MyButton的点击事件发生时,在sendActionsForControlEvents:方法中:

if ([self.delegate respondsToSelector:@selector(buttonDidClick:)]) {
    [self.delegate buttonDidClick:self];
}

这里首先通过respondsToSelector:方法检查委托对象(即ViewController对象)是否实现了buttonDidClick:方法。respondsToSelector:方法在运行时会按照前面介绍的方法查找流程,包括检查协议方法的实现情况。如果ViewController实现了该方法,就会调用[self.delegate buttonDidClick:self],从而执行ViewControllerbuttonDidClick:方法的代码。

动态委托与运行时的灵活性

在运行时,委托对象可以动态改变。例如,可以在MyButton的某个方法中动态设置委托:

- (void)setNewDelegate:(id<ButtonDelegate>)newDelegate {
    self.delegate = newDelegate;
}

然后在ViewController中可以这样使用:

MyButton *anotherButton = [MyButton buttonWithType:UIButtonTypeSystem];
AnotherDelegate *anotherDelegate = [[AnotherDelegate alloc] init];
[anotherButton setNewDelegate:anotherDelegate];

这里AnotherDelegate是另一个遵循ButtonDelegate协议的类。这种动态设置委托的方式充分体现了运行时的灵活性,使得程序在运行过程中可以根据不同的情况选择不同的委托对象来处理事件。

实际应用中的考虑

协议与委托的内存管理

在使用协议与委托时,内存管理是一个重要的考虑因素。通常,委托属性使用weak修饰符,以避免循环引用。例如在MyButton类中:

@property (nonatomic, weak) id<ButtonDelegate> delegate;

如果使用strong修饰符,可能会导致MyButton对象和其委托对象相互强引用,从而导致内存泄漏。当MyButton对象不再被使用时,由于它对委托对象有强引用,委托对象不会被释放;而委托对象如果也对MyButton对象有强引用,那么MyButton对象也不会被释放,形成循环引用。

协议方法的版本兼容性

在开发过程中,可能会对协议进行更新,添加新的方法。如果在协议中添加了新的必需方法,那么所有遵循该协议的类都必须实现这些新方法,否则会导致编译错误。为了避免这种情况对现有代码造成影响,可以将新方法标记为可选方法,或者使用条件编译来处理不同版本的协议。例如:

#ifdef NEW_PROTOCOL_VERSION
@protocol MyProtocol <NSObject>
@required
- (void)oldRequiredMethod;
@optional
- (void)newOptionalMethod;
@end
#else
@protocol MyProtocol <NSObject>
@required
- (void)oldRequiredMethod;
@end
#endif

这样,在旧版本的代码中,可以不定义NEW_PROTOCOL_VERSION,从而不会受到新方法的影响;而在新版本的代码中,定义NEW_PROTOCOL_VERSION,可以使用新的可选方法。

多委托与链式委托

在一些复杂的应用场景中,可能需要多个对象处理同一个事件,这就涉及到多委托(Multiple Delegates)。可以通过定义一个数组来保存多个委托对象,然后在事件发生时依次调用每个委托对象的相应方法。

@interface MultiDelegateButton : UIButton
@property (nonatomic, strong) NSMutableArray<id<ButtonDelegate>> *delegates;
@end

@implementation MultiDelegateButton
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents {
    if (controlEvents & UIControlEventTouchUpInside) {
        for (id<ButtonDelegate> delegate in self.delegates) {
            if ([delegate respondsToSelector:@selector(buttonDidClick:)]) {
                [delegate buttonDidClick:self];
            }
        }
    }
    [super sendActionsForControlEvents:controlEvents];
}
@end

链式委托(Chained Delegation)是另一种扩展委托模式的方式,即一个委托对象在处理事件后,可以将事件传递给下一个委托对象。例如:

@interface ChainedDelegate : NSObject <ButtonDelegate>
@property (nonatomic, weak) id<ButtonDelegate> nextDelegate;
@end

@implementation ChainedDelegate
- (void)buttonDidClick:(UIButton *)button {
    // 处理自己的逻辑
    NSLog(@"ChainedDelegate处理点击事件");
    if (self.nextDelegate && [self.nextDelegate respondsToSelector:@selector(buttonDidClick:)]) {
        [self.nextDelegate buttonDidClick:button];
    }
}
@end

这样可以构建一个委托链,每个委托对象都可以处理一部分逻辑,并将事件传递给下一个委托对象。

协议与委托在框架设计中的应用

在框架设计中,协议与委托模式被广泛应用。例如,在iOS开发的UIKit框架中,UITableView使用委托模式来处理用户与表格的交互。UITableView有一个delegate属性,类型为id<UITableViewDelegate>,通过这个委托,开发者可以实现诸如tableView:didSelectRowAtIndexPath:等方法来处理用户点击表格行的事件。同时,UITableView还遵循UITableViewDataSource协议,用于提供表格展示所需的数据。这种设计使得UITableView具有很高的灵活性,开发者可以根据自己的需求定制表格的行为和数据展示。

在设计框架时,合理使用协议与委托模式可以将不同的功能模块解耦,提高代码的可维护性和可扩展性。例如,一个网络请求框架可以通过协议定义数据解析的方法,让使用者根据自己的数据格式实现相应的解析逻辑,而框架只负责网络请求的发送和接收。这样,框架可以适应不同的数据格式需求,而不需要为每种数据格式都提供特定的实现。

总之,深入理解Objective-C协议与委托在运行时的工作方式,对于编写高质量、灵活且可维护的Objective-C代码至关重要。无论是在小型应用开发还是大型框架设计中,合理运用协议与委托模式都能带来诸多好处,提高开发效率和代码质量。在实际应用中,需要综合考虑内存管理、版本兼容性、多委托等因素,以充分发挥协议与委托模式的优势。通过不断实践和总结经验,开发者能够更好地利用这一强大的特性,开发出更优秀的Objective-C应用程序。同时,随着Objective-C语言的不断发展和演进,协议与委托模式也可能会有新的特性和应用场景出现,开发者需要持续关注并学习,以保持技术的领先性。