Category的原理探索

/ 0

使用场景

分类底层结构

objc4 源码中,可以拿到分类的底层实现:

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; // 属性列表
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);

    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

在程序编译时,我们添加的所有分类都会转换成上面这种结构。在程序运行时,才会通过 Runtime 动态的将分类中的数据合并到原始类。

除了下载 objc4 源码,我们还可以直接通过下面命令将 OC 代码转换为 C++ 代码,也可以验证分类的结构:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

category_t 结构体中可以看出,我们可以在分类中添加对象方法类方法协议属性

并且我们发现分类结构体中不存在成员变量的,因此 分类是不允许添加成员变量 的。分类中添加的属性并不会帮组我们自动生成成员变量,只会生成 get set 方法的声明,需要我们自己去实现。

Category加载过程

通过 Runtime 加载某个类的所有 Category 数据。

把所有 Category对象方法类方法协议属性数据,合并到一个大数组中,后面参与编译的 Category 数据,会在数组的前面。

将合并后的分类数据(对象方法类方法协议属性),插入到类原来数据的前面。

Category和Extension的区别

Extension 在编译时,它的数据就已经包含在类信息中。

Category 在运行时,才会将数据合并到类信息中。

+load方法

+load 方法会在 Runtime 加载类、分类时调用(在 main 函数之前)。

每个类、分类的 +load,在程序运行过程中只调用一次。

类中的 +load 方法不会被分类中的 +load 方法覆盖。

父类中的 +load 方法也不会被子类覆盖,所以子类中的 +load 方法无需手动调用父类的 +load 方法。

+load 方法是根据方法地址直接调用,并不是经过 objc_msgSend 函数调用。

调用顺序

+initialize方法

+initialize 方法会在类第一次接收到消息时调用(newalloc 或调用任何方法时)。

类中的 +initialize 方法被分类中的 +initialize 方法覆盖。

+initialize 方法是通过 objc_msgSend 进行调用的。

调用顺序

Category 能否添加成员变量?

不能直接给 Category 添加成员变量,但是可以间接实现 Category 有成员变量的效果。

分类添加属性

在类或 Extension 里添加属性,会自动生成成员变量get set 方法的声明和实现。

但是在 Category 里添加属性,只会生成 get set 方法的声明。

可以通过维护一个全局的字典来实现成员变量的效果,但是存在很多问题。比如线程安全、内存泄露等。

@interface JFPerson (Test)
@property (nonatomic, assign) int age;
@end

@implementation JFPerson (Test)

NSMutableDictionary *ageDict_;

+ (void)load
{
    ageDict_ = [NSMutableDictionary dictionary];
}

- (void)setAge:(int)age
{
    NSString *key = [NSString stringWithFormat:@"%p", self];
    ageDict_[key] = @(age);
}

- (int)age
{
    NSString *key = [NSString stringWithFormat:@"%p", self];
    return [ageDict_[key] intValue];
}

@end

关联对象

上面使用字典的方式明显是不可取的,但还有一种更科学的方式,就是使用关联对象来间接实现:

@interface JFPerson (Test)
@property (nonatomic, assign) int age;
@end

@implementation JFPerson (Test)

NSMutableDictionary *ageDict_;

- (void)setAge:(int)age
{
    // 添加关联对象
    objc_setAssociatedObject(self, @"age", @(age), OBJC_ASSOCIATION_ASSIGN);
}

- (int)age
{
    // 获得关联对象
    return [objc_getAssociatedObject(self, @"age") intValue];
}

@end

关联对象

objc4 的源码 Apple 公司也一直在维护更新,所有有些老的资料里的分析过程可能已经不适用了。

实现关联对象技术的核心对象有下面 4 个:

大概原理就是 AssociationsManager 是一个管理类,用于获取 AssociationsHashMap

AssociationsHashMap 中存储了关联对象字典 ObjectAssociationMap

ObjectAssociationMap 关联对象字典中又存储了具体的数据 ObjcAssociation

图示