category&&关联对象

112 阅读4分钟

category原理

如何查看编译之后的分类数据结构
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ZJPerson+Test.m
对对应的.m文件执行改指令生成c c++源码
category底层数据结构为_category_t的结构体
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};

然后通过runtime运行时机制,会在程序启动的时候动态把分类相关的数据合并到对应的类的结构体里面去,
这个我们是通过查看源码能够证明的,
我这边是用的781版本

oc源码下载地址(https://opensource.apple.com/tarballs/objc4/)

首先我们找到objc-os.mm文件(程序入口就是在该文件)
找到函数void _objc_init(void) 
找到关键代码 _dyld_objc_notify_register(&map_images, load_images, unmap_image);						
其中map_images就是代表加载一些相关的镜像,这些镜像包括类,分类等等,点进去查看

找到关键代码attachCategories 添加所有分类,
进去该函数会发现拿到类对象的所有数据以及对应的分类的所有数据,
在attachLists这个函数里面会把分类的数据加载到类对象或元类对象的方法列表里面
当然还有属性和协议等等,不过再加载方法的时候会把类对象本身的方法放到方法数组的最后
把分类的方法放到数组的前面,这个就是为什么分类的方法会优先调用,
这个方法列表的里面加载的方法的顺序还和编译顺序有关,先编译先处理,
所以如果分类实现了和对应的类同样的方法,
最后一个编译的分类的方法反而会最先调用

+(void)load和+(void)initialize

+load方法会在runtime加载类、分类时调用,每个类、分类的+load,在程序运行过程中只调用一次
按照编译先后顺序调用(先编译,先调用)
会优先调用类的load方法,然后调用分类的load方法
调用子类load方法的时候会优先调用父类的load方法

initialize会在类第一次接收的消息的时候调用
调用子类的initialize方法的时候也会先调用父类的initialize方法
因为initialize是通过消息发送机制objc_msgSend进行调用的,
如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
如果分类实现了+initialize,会覆盖类本身的+initialize调用

load方法底层原理

上面在说category原理的时候有个load_images
这个就是用来加载load方法的
prepare_load_methods里面_getObjc2NonlazyClassList会加载所有类信息
然后schedule_class_load里面schedule_class_load(cls->superclass)
这里递归调用,验证调用子类load方法的时候会先调用父类的load方法

找到关键函数call_load_methods();

你会发现一目了然,果然是先加载的类的load方法call_class_loads();
然后分类的load方法call_category_loads

 看到里面会对prepare_load_methods里面得到的类信息的数组进行处理
 会直接拿到类对象cls指针以及对应的typedef void(*load_method_t)(id, SEL);指针函数
 直接进行调用(*load_method)(cls, @selector(load)); 
 所以类以及分类的每个load方法都会调用,因为不是走的消息发送的流程
 

+(void)initialized底层原理

还是查看源码找到关键函数class_getInstanceMethod
发现lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
然后里面会判断是否需要initialized
 if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
然后initializeAndMaybeRelock(cls, obj, lock, true);	
initializeNonMetaClass(nonmeta);在改函数里面会发现递归调用
if (supercls  &&  !supercls->isInitialized()) {
    initializeNonMetaClass(supercls);
}
找到关键函数
void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}
证实+initialize是通过消息发送机制来进行调用的

关联对象

根据category_t编译后的数据结构以及源码
发现分类是没发直接添加成员变量的根本没有该数据结构
而且分类添加的属性也只默认进行了setget方法的申明
并没有进行setget方法的实现
所以我们只能间接实现成员变量的功能
就是用到了关联对象
关联对象也是runtime里面的API主要有三个
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) 

关联对象底层原理

AssociationsManager
	|
AssociationsHashMap  
	|
ObjectAssociationMap
	|
ObjcAssociation{
	uintptr_t _policy;
    id _value;
}

所有的关联对象都是由一个全局AssociationsManager统一管理,并不存在对象本身内存中
然后会根据传入的object生成一个和key上传的DisguisedPtr然后把其他数据存入ObjectAssociationMap,
然后组成了AssociationsHashMap,
然后在ObjectAssociationMap里面会根据传入的key存值ObjcAssociation(association{policy, value})
里面存着需要保存的策略以及相关的value
根据下面的源码查看当value设置空的时候我们会发现associations.erase(refs_it);
会直接擦除相关的数据从AssociationsHashMap里面
所以如果你想单独清楚某个关联对象直接对value设置为空即可
关联对象源码查看轨迹如下
objc_setAssociatedObject->_base_objc_setAssociatedObject->_object_set_associative_reference

objc_getAssociatedObject->_object_get_associative_reference

objc_removeAssociatedObjects->_object_remove_assocations

如果需要清楚所有关联对象调用objc_removeAssociatedObjects即可
他会遍历AssociationsHashMap然后擦除所有数据