1.Category底层结构是什么?
Category的实现原理
- category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息。
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象,元类对象中)
-
Category加载过程: 1.通过Runtime加载某个类的所有Category数据。 2.把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面。 3.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面。(Build Phases --> Compile Sources控制编译顺序,最上面的先参与编译)
4.怎样合并到类信息的大数组中呢?是先扩容,然后通过 memmove和memcopy 两个C函数拷贝过去,memcopy如果被拷贝的和目标地址有重合的地方,可能会造成不正确。但是效果高些。在此不扩展深入。
-
所以,当一个分类和这个类有相同的方法,会覆盖原来的方法。但是这个覆盖不是真正的覆盖,只不过在他前面调用,后面的不会被调用。
-
分类也可以遵从协议,也可以增加属性。
和类 Extension的不同
Class Extension(作用和.m里的匿名类别作用一致,其实叫匿名类别不正确,和Category有本质的区别)在编译的时候,它的数据就已经包含在类信息中, Category是在运行时,才会将数据合并到信息中。
2. +load 方法
-
+load方法是runtime把类,分类载入内存的时候调用。就算不显示的代码调用,也会被调用。
-
+load 方法是直接调用,而不是消息发送机制。
源码: runtime入口函数:
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
load_images --> call_load_methods --> call_class_loads
先 call_class_loads 再 call_category_loads
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
直接取+load方法的内存地址调用,不是消息发送机制。
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
- +load方法会在runtime加载类、分类时调用。
- 每个类、分类的+load,在程序运行过程中只调用一次。
- 调用顺序:
-
先调用类的 +load(所有需要加载的类)
- 按照编译先后顺序(先编译,先调用)
- 调用子类的+load之前会先调用父类的+load
-
再调用分类的+load
按照编译先后顺序调用(先编译,先调用)
-
注意不是 类1+load -> 分类1+load --> 类2+load --> 分类2+load. 而是 类1+load -> 类2+load -> 分类1+load -> 分类2+load.
-
阅读源码:
- 手动调用的话:[MyStudent load],就是消息发送机制。所以load方法可以继承。
3.+initialize方法(类初始化)
-
+initialize方法会在类第一次接收到消息时调用
-
调用+initialize是消息发送机制
-
调用顺序
- 先调用父类的+initialize,再调用子类的+initialize(调用子类的时候源码里主动先调用了父类的+initialize)
- 先初始化分类,再初始化子类,每个类只会初始化1次,
-
+load和+initialize的最大区别是,+initialize是通过objc_megSend进行调用的,所以有以下特点:
- 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被多次调用) 2.如果分类实现了+initialize,就会覆盖本身的+initialize调用。
源码解读过程:
- 第一次发送消息,寻找方法的时候,会判断该类有没有初始化,就去判断父类有没有初始化,如果没有就先 调用父类的initialize,然后调用自身的initialize.initialize也是发送消息。如果子类已经初始化了,就无需判断父类是否已经初始化,里面逻辑都不会走了。
4.关联对象
属性
- 对类增加一个属性,做了三件事:生成一个成员变量,生成setter,getter方法声明,生成setter,getter方法实现。
- 对分类增加一个属性,只会增加setter,getter方法声明。(没有成员变量和实现)
- 分类不可以增加成员变量。struct category_t 结构里没有成员变量列表。
但可以通过关联对象来间接的实现给分类添加成员变量。
关联对象提供了以下API
-
添加关联对象
void objc_setAssociatedObject(id object,const void *key,id value, objc_AssociatePolicy policy) -
获取关联对象
id objc_getAssociatedObject(id object,objc_AssociatePolicy policy) -
移除所有关联对象
void objc_removeAssociatedObjects(id object)
四种key的方案
- static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_getAssociatedObject(obj, MyKey)
- static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_getAssociatedObject(obj, &MyKey)
- 使用属性名作为key
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); objc_getAssociatedObject(obj, @"property");
思考:@"name" 放在MachO文件的哪个段?
- 使用get方法的@selecor作为key
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_getAssociatedObject(obj, @selector(getter))
可以用_cmd代替。每个对象方法都有两个隐式参数 self 和 _cmd.
例如:
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
// 隐式参数
// _cmd == @selector(name)
return objc_getAssociatedObject(self, _cmd);
}
objc_AssociatePolicy
源码与原理
-
关联对象存储在全局唯一的一个 AssociationManager中。
-
AssociationManager 有一个Map. key为对象地址,value为这个对象的关联对象Map. ObjectAssociationMap.
-
ObjectAssociationMap中key为设置的key,value为ObjectAssociation.
-
ObjectAssociation里有value和policy.
5.设置关联对象为nil,就相当于移除关联对象。
MJPerson *person2 = [[MJPerson alloc] init];
person2.name = nil;
可以查看源码,ObjectAssociationMap的key对应的value为nil的时候,会有个擦除操作,会把这个key-value移除。
其他
- 也可以给类对象设置关联对象达到添加成员变量的目的。但是因为类对象只有一个,会有冲突。
objc_setAssociatedObject([MyPerson class], @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
- 添加关联对象不会对原来实例对象的结构和类对象的结构造成改变。
MJPerson *person = [[MJPerson alloc] init];
person.age = 10;
person.name = @"jack";
person.weight = 30;
{
MJPerson *temp = [[MJPerson alloc] init];
objc_setAssociatedObject(person, @"temp", temp, OBJC_ASSOCIATION_ASSIGN);
}
NSLog(@"%@", objc_getAssociatedObject(person, @"temp"));
1.大括号里设置ObjectAssociationMap里一个key为 @“temp”的内存地址,value为temp对象。但temp的作用域只限于大括号,出了大括号后,temp就销毁了,这时候value里依然存储这这块内存地址,所以取的时候就会发生
坏内存访问,就会崩溃。说明 id value对temp是强引用。如果是弱引用,temp销毁后就会把value置空,value = nil.
2.objc_setAssociatedObject(person, @"temp", temp, OBJC_ASSOCIATION_ASSIGN);对person不存在引用。