目录
实现原理
Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息,在程序运行的时候,runtime将Category的数据,合并到类信息中(类对象、元类对象中)。
定义在objc-runtime-new.h中的Category的结构
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);
};
通过runtime加载某个类的所有Category数据,把所有Category的方法、属性、协议数据,合并到一个大数组中,后边参与编译的Category数据,会在数组的前面。将合并后的分类数据(方法、属性、协议),插入到类原来的数据前面。所以分类里边的方法会覆盖类原来的方法,分类里边后编译的文件里的方法会覆盖前边编译的方法
文件的编译顺序
在三个文件里同时实现下面的方法:
-(void)test{
NSLog(@"%s",__func__);
}
查看调用日志
-[Person(eat) test]
+load方法
+load方法会在runtime加载类、分类时调用,每个类、分类的+load方法,在程序运行过程中只调用一次。
调用顺序:
1.先调用类的+load方法,类的+load方法的调用顺序按类的编译顺序调用,先编译先调用。但在调用之前会先调用父类的+load方法。
2.再调用分类的+load方法,分类的+load方法的调用顺序按分类的编译顺序调用,先编译先调用。
文件的编译顺序
在五个文件里同时实现下面的方法:
+(void)load{
NSLog(@"%s",__func__);
}
查看日志调用
+[Person load]
+[Student load]
+[Person(run) load]
+[Person(eat) load]
+[Student(study) load]
+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用
+initialize方法
+initialize方法会在类第一次接收到消息时调用。
调用顺序:
先调用父类的+initialize方法,再调用子类的+initialize方法。(先初始化父类,再初始化子类,每个类只初始化一次)。
分类的+initialize方法会覆盖类的+initialize方法。
后编译的分类的+initialize方法会覆盖先编译的分类的+initialize方法。
文件的编译顺序
在五个文件里同时实现下面的方法:
+(void)initialize{
NSLog(@"%s",__func__);
}
书写下面代码
[Person alloc];
[Student alloc];
查看日志调用
+[Person(eat) initialize]
+[Student(study) initialize]
如果将Student及Student+study文件内的+initialize方法注释掉
查看日志调用
+[Person(eat) initialize]
+[Person(eat) initialize]
+initialize方法是在类第一次接收到消息时调用,调用机制是通过objc_msgSend实现的,所以子类没有实现+initialize方法时,会调用到父类的+initialize方法。
+initialize和+load的区别
1.+load只调用一次,+initialize至少调用一次。
2.+load是在类、分类加载时直接通过函数地址调用,+initialize方法是在第一次给类对象发消息时调用,通过objc_msgSend调用。
关联对象
一、默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。
如下Person+run分类
@interface Person (run)
@property (nonatomic, assign) int height;
@end
如下调用时,报错,找不到setHeight:方法,即没有自动生成setHeight:方法
Person * person = [[Person alloc] init];
person.height = 180;
*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[Person setHeight:]: unrecognized selector sent to instance 0x2815d9220’
那我们自己实现下看能不能行。
-(void)setHeight:(int)height{
NSLog(@"%s",__func__);
}
查看日志调用,证明方法已经调用
-[Person(run) setHeight:]
但当我们将成员变量赋值时
或者直接声明成员变量时
二、替代思路
Person+run.h文件
#import <Foundation/Foundation.h>
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (run)
@property (nonatomic, assign) int height;
@end
NS_ASSUME_NONNULL_END
Person+run.m
#import "Person+run.h"
#define LCKey [NSString stringWithFormat:@"%p", self]
NSMutableDictionary * heights_;
@implementation Person (run)
+(void)load{
heights_ = [NSMutableDictionary new];
}
-(void)setHeight:(int)height{
[heights_ setObject:@(height) forKey:LCKey];
}
-(int)height{
return [[heights_ objectForKey:LCKey] intValue];
}
@end
调用
Person * person = [[Person alloc] init];
person.height = 180;
NSLog(@"person.height = %d",person.height);
Person * person1 = [[Person alloc] init];
person1.height = 181;
NSLog(@"person1.height = %d",person1.height);
调用日志
person.height = 180
person1.height = 181
三、关联对象
关联对象的实现方式
Person+run.m
#import <objc/runtime.h>
- (void)setWeight:(int)weight{
objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)weight{
// _cmd == @selector(weight)
return [objc_getAssociatedObject(self, _cmd) intValue];
}
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 |
关联对象的原理
行者常至,为者常成!