使用场景
- 可以减少单个文件的体积。
- 可以按照功能分组,放到不同的分类里,使类结构更清晰。
- 降低耦合性,同一个类可以有多个开发人员进行开发。
- 模拟多继承。
- 把静态库的私有方法公开。
- 可以给系统类新增功能。
分类底层结构
在 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
函数调用。
调用顺序
- 先调用类的
+load
,再调用分类的+load
。 - 调用子类的
+load
之前会先调用父类的+load
。 - 按照编译先后顺序调用(先编译,先调用)。
+initialize方法
+initialize
方法会在类第一次接收到消息时调用(new
、alloc
或调用任何方法时)。
类中的 +initialize
方法会
被分类中的 +initialize
方法覆盖。
+initialize
方法是通过 objc_msgSend
进行调用的。
调用顺序
- 调用子类的
+initialize
之前会先调用父类的+initialize
。 - 如果子类没有实现
+initialize
,会调用父类的+initialize
(所以父类的+initialize
可能会被调用多次)。
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
:存储ObjectAssociationMap
字典。ObjectAssociationMap
:存储ObjcAssociation
字典。ObjcAssociation
:关联对象存储的具体数据。
大概原理就是 AssociationsManager
是一个管理类,用于获取 AssociationsHashMap
。
AssociationsHashMap
中存储了关联对象字典 ObjectAssociationMap
。
ObjectAssociationMap
关联对象字典中又存储了具体的数据 ObjcAssociation
。