分类提出问题 (Category为什么只能加方法不能加属性。)
Category的本质
那么当调用分类的方法时,步骤是否和调用对象方法一样呢? 分类中的对象方法依然是存储在类对象中的,同对象方法在同一个地方,那么调用步骤也同调用对象方法一样。如果是类方法的话,也同样是存储在元类对象中。
那么分类方法是如何存储在类对象中的,我们来通过源码看一下分类的底层结构。
Category的底层结构
如何验证上述问题?通过查看分类的源码我们可以找到category_t 结构体。
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);
};
从源码基本可以看出我们平时使用categroy的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式。并且我们发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。
通过源码我们发现,分类的方法,协议,属性等好像确实是存放在categroy结构体里面的,那么他又是如何存储在类对象中的呢? 我们来看一下底层的内部方法探寻其中的原理。 首先我们通过命令行将Preson+Test.m文件转化为c++文件,查看其中的编译过程。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Preson+Test.m
在分类转化为c++文件中可以看出_category_t结构体中,存放着类名,对象方法列表,类方法列表,协议列表,以及属性列表。

紧接着,我们可以看到_method_list_t类型的结构体,如下图所示

上图中我们发现这个结构体_OBJC_$_CATEGORY_INSTANCE_METHODS_Preson__Test从名称可以看出是INSTANCE_METHODS对象方法,并且一一对应为上面结构体内赋值。我们可以看到结构体中存储了方法占用的内存,方法数量,以及方法列表。并且从上图中找到分类中我们实现对应的对象方法,test , setAge, age三个方法 接下来我们发现同样的_method_list_t类型的类方法结构体,如下图所示

接下来是协议方法列表


属性列表结构体_OBJC__PROP_LIST_Preson__Test同_prop_list_t结构体对应,存储属性的占用空间,属性属性数量,以及属性列表,从上图中可以看到我们自己写的age属性。 最后我们可以看到定义了_OBJC_$_CATEGORY_Preson__Test结构体,并且将我们上面着重分析的结构体一一赋值,我们通过两张图片对照一下。


上下两张图一一对应,并且我们看到定义_class_t类型的OBJC_CLASS__Preson结构体,最后将_OBJC_$_CATEGORY_Preson__Test的cls指针指向OBJC_CLASS__Preson结构体地址。我们这里可以看出,cls指针指向的应该是分类的主类类对象的地址。
通过以上分析我们发现。分类源码中确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中。接下来我们在回到runtime源码查看catagory_t存储的方法,属性,协议等是如何存储在类对象中的。
首先来到runtime初始化函数


从上述代码中我们可以知道这段代码是用来查找有没有分类的。通过_getObjc2CategoryList函数获取到分类列表之后,进行遍历,获取其中的方法,协议,属性等。可以看到最终都调用了remethodizeClass(cls);函数。我们来到remethodizeClass(cls);函数内部查看。

通过上述代码我们发现attachCategories函数接收了类对象cls和分类数组cats,如我们一开始写的代码所示,一个类可以有多个分类。之前我们说到分类信息存储在category_t结构体中,那么多个分类则保存在category_list中。
我们来到attachCategories函数内部。

上述源码中可以看出,首先根据方法列表,属性列表,协议列表,malloc分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。之后从分类数组里面往三个数组里面存放分类数组里面存放的分类方法,属性以及协议放入对应mlist、proplists、protolosts数组中,这三个数组放着所有分类的方法,属性和协议。 之后通过类对象的data()方法,拿到类对象的class_rw_t结构体rw,在class结构中我们介绍过,class_rw_t中存放着类对象的方法,属性和协议等数据,rw结构体通过类对象的data方法获取,所以rw里面存放这类对象里面的数据。
之后分别通过rw调用方法列表、属性列表、协议列表的attachList函数,将所有的分类的方法、属性、协议列表数组传进去,我们大致可以猜想到在attachList方法内部将分类和本类相应的对象方法,属性,和协议进行了合并。 我们来看一下attachLists函数内部。

// memmove :内存移动。
/* __dst : 移动内存的目的地
* __src : 被移动的内存首地址
* __len : 被移动的内存长度
* 将__src的内存移动__len块内存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :内存拷贝。
/* __dst : 拷贝内存的拷贝目的地
* __src : 被拷贝的内存首地址
* __n : 被移动的内存长度
* 将__src的内存拷贝__n块内存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
下面我们图示经过memmove和memcpy方法过后的内存变化。

经过memmove方法之后,内存变化为
// array()->lists 原来方法、属性、协议列表数组
// addedCount 分类数组长度
// oldCount * sizeof(array()->lists[0]) 原来数组占据的空间
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));

经过memmove方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置。
memcpy方法之后,内存变化
// array()->lists 原来方法、属性、协议列表数组
// addedLists 分类方法、属性、协议列表数组
// addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));

我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmove和memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。 那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。 其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。我们可以通过打印所有类的所有方法名来查看
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ - %@", cls, methodNames);
}
- (void)viewDidLoad {
[super viewDidLoad];
Preson *p = [[Preson alloc] init];
[p run];
[self printMethodNamesOfClass:[Preson class]];
}
通过下图中打印内容可以发现,调用的是Test2中的run方法,并且Person类中存储着两个run方法。

load 和 initialize
load
+load()方法会在runtime加载类和分类到内存中的时候调用,而且每个类或者分类的+load()方法只会调用一次。而且如果同时存在子类和分类的情况下,会先调用父类的+load()方法,再调用子类的+load()方法,最后调用分类的+load()方法。下面我们通过源码来验证这一结论。
先父类再子类
当获取到所有类的集合之后,通过遍历来调用schedule_class_load函数来递归调度类的+load方法和所有它的父类的+load方法,而且通过schedule_class_load(cls->superclass);这句可以看出,在调度+load方法是,是先父类再子类的顺序,如下
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
先本类再分类

直接拿到load方法的内存地址直接调用方法


我们可以看到分类中也是通过直接拿到load方法的地址进行调用。因此正如我们之前试验的一样,分类中重写load方法,并不会优先调用分类的load方法,而不调用本类中的load方法了。
initialize
+initialize执行时机

initializeNonMetaClass函数就是最核心的函数,它的作用就是根据需要向任意的未初始化的类发送一个“+initialize”消息,并且会首先执行超类的初始化,具体实现查看以下源码,源码中省略了部分关于初始化状态设置的一些代码,保留了核心的函数调用,如果想看完整代码,可以自行查看最新的objc4的源码
void initializeNonMetaClass(Class cls)
{
assert(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
//此处通过cls->superclass来找到cls的父类,然后通过递归来查看父类是否被初始化,从而确保在初始化cls之前,它的父类已经初始化完毕
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
{
monitor_locker_t lock(classInitLock);
//如果当前类并未初始化,则设置类的状态为“正在初始化”
if (!cls->isInitialized() && !cls->isInitializing()) {
cls->setInitializing();
reallyInitialize = YES;
}
}
......
//如果当前的类未进行初始化,则调用callInitialize进行初始化
if (reallyInitialize) {
callInitialize(cls);
return;
}
......
}
关联对象 (给分类添加成员变量)
默认情况下,由于分类的底层结构的限制,不能在分类中添加成员变量,但是我们可以通过runtime提供的Api为类增加关联对象。
关联对象的实现
关联对象主要通过以下三个函数进行实现
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。 参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。 参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。 参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。 有以下几种
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性
};
- 添加关联对象
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)
- 以一个简单的例子来看关联对象的实现
@interface XLPerson (Test)
@property(nonatomic, copy)NSString *name;
@end
@implementation XLPerson (Test)
- (NSString *)name{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}
@end
- 使用关联对象时会用到objc_AssociationPolicy,它其实和我们OC中的属性修饰符一一对应,使用什么修饰符,取决于你定义的属性的类型,对应关系如下
| objc_AssociationPolicy | 修饰符 |
|---|---|
| OBJC_ASSOCIATION_ASSIGN | assign |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
| OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
| OBJC_ASSOCIATION_RETAIN | strong, atomic |
| OBJC_ASSOCIATION_COPY | copy, atomic |
- 我们会发现其中只有RETAIN和COPY而为什么没有weak呢? 对源码的分析我们知道,object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object。
disguised_ptr_t disguised_object = DISGUISE(object);
而同时我们知道,weak修饰的属性,当没有拥有对象之后就会被销毁,并且指针置位nil,那么在对象销毁之后,虽然在map中既然存在值object对应的AssociationsHashMap,但是因为object地址已经被置位nil,会造成坏地址访问而无法根据object对象的地址转化为disguised_object了。
关联对象实现原理
实现关联对象技术的核心对象有
AssociationsManager AssociationsHashMap ObjectAssociationMap ObjcAssociation 其中Map同我们平时使用的字典类似。通过key-value一一对应存值。
对关联对象技术的核心对象有了一个大概的意识,我们通过源码来探寻这些对象的存在形式以及其作用。
在runtime源码,首先找到objc_setAssociatedObject(函数,看一下其实现
关联对象图表

-
通过上图我们可以总结为:一个实例对象就对应一个ObjectAssociationMap,而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,为ObjcAssociation中存储着关联对象的value和policy策略。
-
由此我们可以知道关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map用来存放每一个对象及其对应关联属性表格。
小结
CCategory
CCategory 原理
- 分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
- Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。
- 通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。
Category为什么只能加方法不能加属性?
Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。
Category 方法加载顺序
最后参加编译的分类方法会替换掉前面分类或本类方法的实现。
load 和 initialize
load 方法
- 本质上是调用函数的内存地址。
- +load方法是在runtime加载类、分类是进行调用的,在程序运行过程中只会调用一次
- 如果存在多个子类,则会先调用父类的+load方法,再调用子类的+load方法,子类+load方法的调用顺序和子类的编译顺序相同,先编译的子类优先调用。
- 如果同时存在多个分类和多个子类,那么首先会调用父类的+load方法,再调用子类的+load方法,最后才会调用分类的+load方法,多个分类+load方法的调用顺序和编译顺序相同,先编译的分类先调用。
initialize
- initialize方法是在类第一次接收到消息时调用
- 本质还是消息转发机制。
- 如果存在多个子类,并且实现了+initialize方法,会先初始化父类,调用父类的+initialize方法,然后再初始化子类,调用子类的+initialize方法.
- 如果子类没有实现+initialize方法,则会调用父类的+initialize方法,所以父类的+initialize可能会被调用多次。
- 如果分类实现了+initialize方法,则会覆盖类本身的+initialize调用,因为+initialize方法的调用本质上是通过objc_msgSend来发送一个+initialize消息,所以一旦分类实现了+initialize方法,则会将分类的+initialize方法插入到类方法列表的最前面,会覆盖原来类的+initialize方法。
关联对象的目的
虽然在分类中可以写@property 添加属性,但是不会自动生成私有属性,也不会生成set,get方法的实现,只会生成set,get的声明,需要我们自己去实现。
关联对象实现原理
关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map用来存放每一个对象及其对应关联属性表格。