本文并不是一篇完整的教程,更像一篇快速笔记,讲解 Objective-C 中的分类。

Objective C

分类 Category

通过分类给类添加方法

分类提供了一个简单的方式,用它可以将类的定义模块化到相关方法的分类中;它还提供了扩展现有类定义的简便方式,并且不必访问类的源代码,也无须创建子类。如下通过分类来扩展 DTShape 新增 sum 方法:

#import "DTShape.h"

@interface DTShape (MathOps)

- (int)sum:(int)length;

@end

在分类的实现中也可以访问类中属性和方法:

#import "DTShape+MathOps.h"

@implementation DTShape (MathOps)

- (int)sum:(int)length {
  return self.numberOfSides * length;
}

@end

要使用分类,需要导入相应的头文件:

#import "DTShape+MathOps.h"

DTNamedShape *shape = [[DTNamedShape alloc] initWithName:@"square"];
shape.numberOfSides = 4;
[shape sum:4]

通过关联对象给类添加属性

分类中不支持添加实例变量,所以没有办法支撑属性,但是可以通过关联对象在没有源代码的情况下来支撑属性,但是不推荐这么做,相关方法:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);

objc_AssociationPolicy 的值如下,可以和 Objective-C 内存管理 的属性修饰符相对应:

  • OBJC_ASSOCIATION_ASSIGN
  • OBJC_ASSOCIATION_COPY
  • OBJC_ASSOCIATION_COPY_NONATOMIC
  • OBJC_ASSOCIATION_RETAIN
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC

示例:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;

- (id)initWithFirstName:(NSString*)firstName 
            andLastName:(NSString*)lastName;
@end

@implementation EOCPerson
@end
@interface EOCPerson (Friendship)
@property (nonatomic, strong) NSArray *friends;
- (BOOL)isFriendsWith:(EOCPerson*)person;
@end
#import <objc/runtime.h>

static const char* kFriendsPropertyKey = "kFriendsPropertyKey";

@implementation EOCPerson (Friendship)

- (NSArray*)friends {
    return objc_getAssociatedObject(self, kFriendsPropertyKey);
}

- (void)setFriends:(NSArray*)friends {
    objc_setAssociatedObject(self, kFriendsPropertyKey, friends, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

类的扩展 Class Extension

创建一个未命名的分类,且在括号 () 之间不指定名字,这是一种特殊的情况,这种特殊的语法定义为类的扩展;未命名分类是非常有用的,因为它们的方法都是私有的,如果需要写一个类,而且属性和方法仅供这个类本身使用,未命名分类比较合适:

@interface PWAbility ()

@property (copy, nonatomic) NSDictionary *commands;

@end

@implementation PWAbility

- (instancetype)init {
  self = [super init];
  if (self) {
    _commands = @{PWTextCommand.msgType: PWTextCommand.class};
  }
  return self;
}

- (PWCommand *)commandWithData:(NSData *)data {
  NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
  NSString *msgType = (NSString *)[json valueForKey:@"msgType"];
  if (!msgType) {
    return nil;
  } else {
    Class class = (Class)self.commands[msgType];
    PWCommand<PWCommandReceivable> *command = [class new];
    [command parseData:json];
    return command;
  }
}

@end

深入分类

要理解 Category 的实现,就需要深入研究 Runtime 这部分的源码,这里参考 深入理解 Objective-C:Category 这遍文章进行了一些关键信息的摘录:

编译 Category

所有的 OC 类和对象,在 Runtime 层都是用 struct 表示的,Category 也不例外,其定义:

typedef struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
} category_t;
  • Category 和 Class Extension 完全是两个东西,Class Extension 在编译期决议,它就是类的一部分,它伴随类的产生而产生,亦随之一起消亡,但是 Category 则完全不一样,它是在运行期决议的。
  • 首先,编译器生成了 Category 中给类添加的实例方法的列表 instanceMethods,类方法的列表 classMethods,协议的列表 protocols,属性的列表 instanceProperties。
  • 其次,编译器生成了 Category 本身 OBJC$_CATEGORYMyClass$_MyAddition,并用前面生成的列表来初始化 Category 本身。有一个需要注意到的事实就是 Category 的名字用来给各种列表以及后面的 Category 结构体本身命名,而且有 static 来修饰,所以在同一个编译单元里我们的 Category 名不能重复,否则会出现编译错误
  • 编译器在 DATA 段下的 objc_catlist section 里保存了一个大小为 1 的 category_t 的数组L_OBJC_LABELCATEGORY$,当然,如果有多个 Category,会生成对应长度的数组,用于运行期 Category 的加载。

加载 Category

  • 在 Objective-C Runtime 库被加载过程中,前面编译阶段产生的 category_t 数组被加载和附加到类上,Category 的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 Category 的方法会 "覆盖" 掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的。
  • 附加 Category 到类的工作会先于 +load 方法的执行,所以在类的 +load 方法调用的时候,我们可以调用 Category 中声明的方法。
  • +load 的执行顺序是先类,后 Category,而 Category 的 +load 执行顺序是根据编译顺序决定的。
  • 但是对于 "覆盖" 掉的方法,则会先找到最后一个编译的 Category 里的对应方法。

load 和 initialize

+ (void)load
+ (void)initialize
  • load 在当每个类和 Category 被加载到 Runtime 时运行,且只运行一次。
  • initialize 在类第一次被使用时,才会运行,且只运行一次。