OC底层学习-Category
- 1. Category
- 1.1分类的简单引用场景
- 1.2 Category编译之后的底层结构
- 1.3 Category源码分析
- 1.4 Category的加载过程和一些注意点
- 2.+load方法
- 3.+initialize方法
- 4. 关联对象
- 4.1关联对象的概述和简单运用
- 4.2 关联对象的底层数据结构
- 5. 面试题
- 5.1 Category的使用场合是什么?
- 5.2 Category的实现原理?
- 5.3 Category和 Class Extension的区别是什么?
- 5.4 Category中又load方法吗?load方法是在什么时候调用的?load方法能继承吗?
- 5.5 load、initialize方法的区别是什么?它们在category中的调用顺序?以及出现继承他们之间的调用过程?
- 5.6 Category能否添加成员变量?如果可以,如何给Category添加成员变量?
1. Category
1.1分类的简单引用场景
@interface GYPerson : NSObject
- (void)run;
@end
@implementation GYPerson
- (void)run {NSLog(@"run===========");
}
@end//分类
@interface GYPerson (Test)
- (void)test;
@end
@implementation GYPerson (Test)
- (void)test {NSLog(@"test==========");
}
@end@interface GYPerson (Eat)
- (void)eat;
@end
@implementation GYPerson (Eat)
- (void)eat {NSLog(@"eat============");
}
@end//测试代码GYPerson *person = [[GYPerson alloc] init];
[person run];
[person test];
[person eat];
上述是一个简单的Category的引用场景
1.2 Category编译之后的底层结构
首先我们使用终端命令把其中一个分类编译成C++文件(编译指令:xcrun -sdk iphoneos clang -arc arm64 -rewrite-objc ob_name(文件名称) -> 直接把c++的文件编译在当前文件夹下),来查看其中分类的结构,
- C++ 文件
//分类的底层结构:
struct _category_t {const char *name; //类名struct _class_t *clsconst 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;//属性 分类中也可以增加属性};//GYPerson+Test的分类结构
static struct _category_t _OBJC_$_CATEGORY_GYPerson_$_Test __attribute__ ((used,
section ("__DATA,__objc_const"))) =
{"GYPerson", 0, // &OBJC_CLASS_$_GYPerson,(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_GYPerson_$_Test,(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_GYPerson_$_Test,0,0,
};//_method_list_t ->_OBJC_$_CATEGORY_INSTANCE_METHODS_GYPerson_$_Teststatic struct {unsigned int entsize; // sizeof(struct _objc_method)unsigned int method_count;struct _objc_method method_list[1];} _OBJC_$_CATEGORY_INSTANCE_METHODS_GYPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {sizeof(_objc_method),1,{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_GYPerson_Test_test}}};//_method_list_t -> _OBJC_$_CATEGORY_CLASS_METHODS_GYPerson_$_Teststatic struct {unsigned int entsize; // sizeof(struct _objc_method)unsigned int method_count;struct _objc_method method_list[1];} _OBJC_$_CATEGORY_CLASS_METHODS_GYPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {sizeof(_objc_method),1,{{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_GYPerson_Test_test2}}};
查看分类转成C++的代码之后, 我们可以发现,当程序编译完成的时候,category
所有的数据都是存放在 category_t
这个底层结构中的,如果有多个分类, 编译完成的时候有多个 category_t
类型的变量,所有分类的结构都是一样的, 但是结构体的名称、结构体内的变量肯定是不一样的
上述描述的GYPerson+Test的C++代码的结构,我们可以猜测GYPerson+Eat的分类在C++ 文件中的结构:
static struct _category_t _OBJC_$_CATEGORY_GYPerson_$_Eat {"GYPerson",0, // &OBJC_CLASS_$_GYPerson,(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_GYPerson_$_Eat,//Eat分类中的对象方法列表(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_GYPerson_$_Eat,//Eat分类中的类方法列表0,//Eat 中的协议列表0,//Eat 中的属性列表}
1.3 Category源码分析
-
结论:
- Category(分类)中的所有对象方法最终都会放在class对象的方法列表中的,调用Category中的方法,最终都会通过instance的isa指针找到class对象,然后找到方法并执行
- Category(分类)中所有的类(+)方法最后都会放在meta-class对象的方法列表中,类方法的调用过程,最后会通过isa的指正找到class对象,在通过class的isa指正找到meta-class对象中,然后寻找类方法执行
- 分类的数据是在程序运行的过程中把Category类中的方法合并到类对象中的,是通过Runtime动态将分类的方法合并到类对象、元类对象中,在编译的时候还没有合并到类对象、元类对象中
-
由于源码太多,会过滤掉不重要源码,显示主要信息的源码:
//运行时的初始话方法
void _objc_init(void)
{
//注册一些_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}map_images(unsigned count, const char * const paths[],const struct mach_header * const mhdrs[])
{rwlock_writer_t lock(runtimeLock);return map_images_nolock(count, paths, mhdrs);
}void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],const struct mach_header * const mhdrs[])
{if (hCount > 0) {//加载模块_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);}}
//加载模块
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{// Discover categories. 搜索和加载我们的分类信息for (EACH_HEADER) {//category_t 分类的结构体 是一个二位数组 category_t **catlist = _getObjc2CategoryList(hi, &count);bool hasClassProperties = hi->info()->hasCategoryClassProperties();for (i = 0; i < count; i++) {category_t *cat = catlist[i];Class cls = remapClass(cat->cls);if (!cls) {// Category's target class is missing (probably weak-linked).// Disavow any knowledge of this category.catlist[i] = nil;if (PrintConnecting) {_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with ""missing weak-linked target class", cat->name, cat);}continue;}// Process this category. // First, register the category with its target class. // Then, rebuild the class's method lists (etc) if // the class is realized. bool classExists = NO;// instanceMethods 实例对象方法 判断if (cat->instanceMethods || cat->protocols || cat->instanceProperties) {addUnattachedCategoryForClass(cat, cls, hi);if (cls->isRealized()) {//核心代码 重新方法话 -> 重新组织这个类中的方法,把class对象传递进去remethodizeClass(cls);classExists = YES;}if (PrintConnecting) {_objc_inform("CLASS: found category -%s(%s) %s", cls->nameForLogging(), cat->name, classExists ? "on existing class" : "");}}// 这个是 重新组织分类中的类方法if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) {addUnattachedCategoryForClass(cat, cls->ISA(), hi);if (cls->ISA()->isRealized()) {// class对象的isa指向meta-class对象,这里参数是传递meta-class对象remethodizeClass(cls->ISA());}if (PrintConnecting) {_objc_inform("CLASS: found category +%s(%s)", cls->nameForLogging(), cat->name);}}}}ts.log("IMAGE TIMES: discover categories");// Category discovery MUST BE LAST to avoid potential races // when other threads call the new category code before // this thread finishes its fixups.
}static void remethodizeClass(Class cls)
{category_list *cats;bool isMeta;runtimeLock.assertWriting();//判断传递进来是class对象还是 meta-class对象isMeta = cls->isMetaClass();// Re-methodizing: check for more categoriesif ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {if (PrintConnecting) {_objc_inform("CLASS: attaching categories to class '%s' %s", cls->nameForLogging(), isMeta ? "(meta)" : "");}//核心方法 把分类中的方法 合并到class对象中attachCategories(cls, cats, true /*flush caches*/); free(cats);}
}//传递参数 ,class对象 category_list: 分类列表 可能装着好几个分类
// class: GYPerson
//cats = [catagory_t (Test),catagory_t(Eat)]结构可能是存放这两个东西
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{if (!cats) return;if (PrintReplacedMethods) printReplacements(cls, cats);bool isMeta = cls->isMetaClass();// fixme rearrange to remove these intermediate allocations// 方法数组method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));// 属性数组property_list_t **proplists = (property_list_t **)malloc(cats->count * sizeof(*proplists));//协议数组protocol_list_t **protolists = (protocol_list_t **)malloc(cats->count * sizeof(*protolists));// Count backwards through cats to get newest categories firstint mcount = 0;int propcount = 0;int protocount = 0;int i = cats->count;bool fromBundle = NO;//开一个循环 i-- while (i--) {//去cats 也就是存放分类的数组中取出某个分类,并且是从后往前取出,也就是 先进后出(先编译的后取出,后编译的先取出)auto& entry = cats->list[i];// entry.cat 取出的是category_t *cat这种类型的对象 也就是category分类结构的对象//取出当前分类中的对象方法method_list_t *mlist = entry.cat->methodsForMeta(isMeta);if (mlist) {//把分类的方法数组放到 class的方法列表数组中/*大概的结构:[[method_t, method_t],[method_t, method_t],]最终每一个分类的方法列表都会放到class的这个大的方法列表中*/mlists[mcount++] = mlist;fromBundle |= entry.hi->isBundle();}property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi);if (proplist) {proplists[propcount++] = proplist;}protocol_list_t *protolist = entry.cat->protocols;if (protolist) {protolists[protocount++] = protolist;}}//cls->data()得到类中的数据 获取class_rw_t对象 ->管理着class对象中的方法列表、属性列表、协议列表等信息,也就是 class对象中的信息auto rw = cls->data();prepareMethodLists(cls, mlists, mcount, NO, fromBundle);//rw->methods 得到rw对象中的方法列表method_array_t methods; 然后调用attachLists方法 把分类所有方法传递进去(mlists: 所有分类对象的方法)//也就是将所有分类的对象方法,附加到类对象的方法列表中rw->methods.attachLists(mlists, mcount);free(mlists);if (flush_caches && mcount > 0) flushCaches(cls);rw->properties.attachLists(proplists, propcount);free(proplists);rw->protocols.attachLists(protolists, protocount);free(protolists);
}void attachLists(List* const * addedLists, uint32_t addedCount) {if (addedCount == 0) return;if (hasArray()) {// many lists -> many listsuint32_t oldCount = array()->count;uint32_t newCount = oldCount + addedCount;setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));array()->count = newCount;// array()->lists 原来的方法列表//将原来的方法列表移动到后面memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));// addedLists 所有分类的方法列表//把分类中的方法列表 copy到原来的方法列表中memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));/*memcpy 方法会一个一个 的复制,然后覆盖原来的值,这里是不会进行判断的统一从小地址开始memmove ()是根据传递参数判断往哪个方法移动, 从哪个地方开始移动,达到最后移动的目的,为什么源码中前面使用memmove() 保证数据能够完整的移动到新的地方*/}
};
源码的解读顺序:
objc-os.mm 表示运行时的入口文件
1.4 Category的加载过程和一些注意点
- Category在编译期还没有合并到某个类的数据中的, 是在运行时通过Runtime加载某个类的所有Category数据
- 在运行时,把所有Category的方法、属性、协议数据,合并到一个大数组中。过程如下
- 首先将原来类对象的方法列表进行一个扩容,然后把原来的方法列表挪到后面
- 在把分类中的方法列表添加到类对象的方法列表中, 最后就是分类中所有的方法都添加到类对象的方法列表中
- 注意:
- category和类中都存在同一个方法的时候,会优先调用category中的方法(由上面源码分析可知)
- 当多个分类都存在同一个方法时,优先调用哪个分类中的方法,取决于category的编译顺序,会优先加载最后编译的category,越是最后编译的category,方法列表等数据信息越在前(由上面源码分析可知)
- 分类中的方法并没有覆盖原来类中的方法,只是方法的执行顺序超前了,因为所有的方法最终都会合并到类对象的方法列表中,而方法执行,只需要找到方法的执行就不会往后面继续找
- 将合并后的分类数据(方法、属性、协议)插入到类原来的数据前面
2.+load方法
- 测试代码:
@interface GYPerson : NSObject@end
@implementation GYPerson
+ (void)load {NSLog(@"GYPerson +load");
}@interface GYPerson (Test1)@end
@implementation GYPerson (Test1)
+ (void)load {NSLog(@"GYPerson(Test1) +load");
}
@interface GYPerson (Test2)@end
+ (void)load {NSLog(@"GYPerson(Test2) +load");
}
@interface GYStudent : GYPerson@end
+ (void)load {NSLog(@"GYStudent +load");
}@interface GYStudent (Test1)@end
@implementation GYStudent (Test1)
+ (void)load {NSLog(@"GYStudent(Test1) +load");
}
@end@interface GYStudent (Test2)@end
@implementation GYStudent (Test2)
+ (void)load {NSLog(@"GYStudent(Test2) +load");
}
@end
执行结果:不管怎么改变顺序GYPerson的+load方法调用始终执行在GYStudent的+load方法之前
- +load方法会在runtime加载类,分类的时候调用
- 每个类、分类的+laod,在程序运行过程中只调用一次
- +load方法调用顺序:
- 首先调类的+laod方法
- 按照变异顺序调用(先编译,先调用)
- 调用子类的+load方法之前会调用父类的+load方法
- 调用完类的+load方法之后,在调用分类的+load方法
- 分类的+load方法顺序也是按照编译顺序调用(先编译,先调用)
- 调用+load方法底层是直接通过指针(地址)直接调用的+load方法(直接通过在内存中的地址直接去调用),而调用分类的类方法,本质上是给对象发送一个消息(也就是通过
objc_msgSend()
),但是通过这种模式(消息机制),它的调用过程是首先通过isa指针找到meta-class对象,然后在去遍历方法列表中的方法,找到方法之后再调用,调用之后接结束不在往下寻找 - +load方法子所以分类和类都会调用,是因为+laod方法的调用是直接通过函数指针指向那个函数,拿到那个函数地址,找到函数的代码,直接调用,是分开调用的,而不是通过
objc_msgSend()
消息发送机制来调用的
- +load方法的底层源码
void _objc_init(void)
{_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
//加载模块
void
load_images(const char *path __unused, const struct mach_header *mh)
{// Return without taking locks if there are no +load methods here.if (!hasLoadMethods((const headerType *)mh)) return;recursive_mutex_locker_t lock(loadMethodLock);// Discover load methods{rwlock_writer_t lock2(runtimeLock);prepare_load_methods((const headerType *)mh);}// Call +load methods (without runtimeLock - re-entrant)call_load_methods();
}
//准备加载方法
void prepare_load_methods(const headerType *mhdr)
{size_t count, i;runtimeLock.assertWriting();classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count);for (i = 0; i < count; i++) {// 加载方法schedule_class_load(remapClass(classlist[i]));}category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);for (i = 0; i < count; i++) {category_t *cat = categorylist[i];Class cls = remapClass(cat->cls);if (!cls) continue; // category for ignored weak-linked classrealizeClass(cls);assert(cls->ISA()->isRealized());add_category_to_loadable_list(cat);}
}static void schedule_class_load(Class cls)
{if (!cls) return;assert(cls->isRealized()); // _read_images should realizeif (cls->data()->flags & RW_LOADED) return;// Ensure superclass-first ordering//递归调用 把类的+load加入数组时 首先把父类的+load方法计入到数组中schedule_class_load(cls->superclass);//把方法加入到方法数组中add_class_to_loadable_list(cls);cls->setInfo(RW_LOADED);
}void add_class_to_loadable_list(Class cls)
{IMP method;loadMethodLock.assertLocked();method = cls->getLoadMethod();if (!method) return; // Don't bother if cls has no +load methodif (PrintLoading) {_objc_inform("LOAD: class '%s' scheduled for +load", cls->nameForLogging());}if (loadable_classes_used == loadable_classes_allocated) {loadable_classes_allocated = loadable_classes_allocated*2 + 16;loadable_classes = (struct loadable_class *)realloc(loadable_classes,loadable_classes_allocated *sizeof(struct loadable_class));}loadable_classes[loadable_classes_used].cls = cls;loadable_classes[loadable_classes_used].method = method;loadable_classes_used++;
}//调用方法
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 morewhile (loadable_classes_used > 0) {call_class_loads();}// 2. Call category +loads ONCEmore_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;
}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方法的地址直接调用率,并不是经过objc-msgSend函数调用/*专门用来加载类的 method只指向load方法struct loadable_class {Class cls; // may be nilIMP method; };专门用来加载分类 method只指向分类load 方法struct loadable_category {Category cat; // may be nilIMP method;};*/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方法的调用过程,category中+load方法调用过程和这个基本相似
- +laod方法调用的源码查看步骤:
3.+initialize方法
-
+initialize()方法会在
类
的第一次接收到消息时调用。 -
调用顺序:
- 先调用父类的+initialize方法,在调用子类的+initialize方法(子类内部会主动调用父类的+initialize(),前提是父类没有初始化)
- 先初始化父类,在初始化子类,每个类只会初始化一次
-
+initialize()和+load的很大区别是+initialize()是通过objc_msgSend进行调用的,所以有以下特点
- 如果子类没有实现+initialize(),会调用父类的+initialize()(所以父类的+initialize()可能会被调用多次)
- 如果分类实现了+initialize(),就会覆盖本身的+initialize()调用(而分类的调用方法顺序是看编译顺序,后编译,先调用 )
由于objc_msgSend()的源码是汇编语言,只能通过其他的方式去验证initialize的调用过程,猜想
initialize方法是在类第一次接收到消息的时候调用那么我们可以查看 这个方法的一些调用过长,我们猜测可能是在第一次接收到方法的时候,内部做了调用+initialize方法的操作
- 源码删减了不需要的
Method class_getInstanceMethod(Class cls, SEL sel)
{//调用方法之前,先去查查找方法lookUpImpOrNil(cls, sel, nil, NO/*initialize*/, NO/*cache*/, YES/*resolver*/);#warning fixme build and search cachesreturn _class_getMethod(cls, sel);
}IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
{IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);if (imp == _objc_msgForward_impcache) return nil;else return imp;
}IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
{//查找方法之前看 这个类有没有被初始化过, 如果需要初始化,单是类还没被初始化 ,那首先就会先初始化类if (initialize && !cls->isInitialized()) {runtimeLock.unlockRead();_class_initialize (_class_getNonMetaClass(cls, inst));runtimeLock.read();// If sel == initialize, _class_initialize will send +initialize and // then the messenger will send +initialize again after this // procedure finishes. Of course, if this is not being called // from the messenger then it won't happen. 2778172}
}//调用initialize方法
void _class_initialize(Class cls)
{assert(!cls->isMetaClass());Class supercls;bool reallyInitialize = NO;// Make sure super is done initializing BEFORE beginning to initialize cls.// See note about deadlock above.//调用类的initialize方法之前,需要判断这个类有没有父类,如果有父类,那会先去调用父类的+initialize方法,前提是父类没有初始化supercls = cls->superclass;if (supercls && !supercls->isInitialized()) {_class_initialize(supercls);}if (reallyInitialize) {// We successfully set the CLS_INITIALIZING bit. Initialize the class.// Record that we're initializing this class so we can message it._setThisThreadIsInitializingClass(cls);if (MultithreadedForkChild) {// LOL JK we don't really call +initialize methods after fork().performForkChildInitialize(cls, supercls);return;}// Send the +initialize message.// Note that +initialize is sent to the superclass (again) if // this class doesn't implement +initialize. 2157218if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",pthread_self(), cls->nameForLogging());}// Exceptions: A +initialize call that throws an exception // is deemed to be a complete and successful +initialize.//// Only __OBJC2__ adds these handlers. !__OBJC2__ has a// bootstrapping problem of this versus CF's call to// objc_exception_set_functions().
#if __OBJC2__@try
#endif{callInitialize(cls);if (PrintInitializing) {_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",pthread_self(), cls->nameForLogging());}}
}void callInitialize(Class cls)
{((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);asm("");
}
源码分析流程图:
- +initialize()和+load()方法使用场景:
- +initialize():需要在类第一次被调用做些事情的时候,可在方法内部做些事情
- +load(): 类被加载内存中的时候需要做事情的时候,可在方法内部做些事情
4. 关联对象
4.1关联对象的概述和简单运用
- 在类中声明
@property (nonatomic, assign) int age;
做了什么事情:- 创建一个_age的成员变量
- 生成一个set方法和get方法的声明
- 生成set和get方法的实现
- 在category声明一个属性,做了什么事情:
- 只是生成了一个set和get方法的声明
- category中不可以声明成员变量
问题:那我们怎么在分类中添加一个属性, 让分类间接实现有成员变量的方式?
- 定义一个全局的变量来储存 声明属性的值,但是这种方式的确定也很明显,如果创建多个实例的话,那就容易造成多个实例共用一个属性值的问题,所以这种方法不可行
@interface GYPerson (Test)/** <#descrption#> */
@property (nonatomic, assign) int weight;@end
@implementation GYPerson (Test)
int weight_;- (void)setWeight:(int)weight {weight_ = weight;
}- (int)weight {return weight_;
}
@end
- 定义一个全局的字典来储存,实现对象和睡醒一对一的关系,这种方法可以实现,也可行,但是还是存在一些问题:
- 如果你的需求不是要求这个类一直存在, 那么就会存在内存泄漏的问题
- 线程安全的问题
- 过程比较麻烦,没增加一个属性都需要对象增加一个全局的字典来储存
@implementation GYPerson (Test2)
NSMutableDictionary *names_;+ (void)load
{names_ = [NSMutableDictionary dictionary];
}- (void)setName:(NSString *)name
{NSString *key = [NSString stringWithFormat:@"%p", self];names_[key] = name;
}
- (NSString *)name
{NSString *key = [NSString stringWithFormat:@"%p", self];return names_[key];
}@end
- 使用关联对象的方式来完成 Runtime关联属性,关联API:
//设置关联对象
/**参数列表: 1. 给哪个对象设置关联对象2. 传递一个任意指针类型的参数 key3. 需要关联的值value4. 关联策略(如:objc_AssociationPolicy中的类型 OBJC_ASSOCIATION_ASSIGN等)*/
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)//获取需要关联的值
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
- 关联中的key的用法:
- 定义一个指针变量
key const void *key
;如果没有初始化的可能造成共用一个属性值,可以static const void * key = &key
(如果不用static修改限定变量的范围,那别人可以在外部使用extern字段用用字段,然后在外面修改这个值) - 定义char类型的变量
static const char KEY
因为char类型的变量只占一个字节,相比于直接定义指针类型的变量(占8个字节)节省内存,不需要赋值,因为传递进去的需要的变量的地址值,而不是变量的值 - 或则key的值直接传递
@“key”
,因为直接写一个字符串,这个字符串是放在常量区的,是不会变的,因为我们字符串声明就NSString *p = @"key";
- 直接传递一个方法对象@selector(key) 或则 _cmd(表示当前方法的selector对象)
- 定义一个指针变量
//直接传递字符串
#define MJNameKey @"name"
- (void)setName:(NSString *)name
{objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{return objc_getAssociatedObject(self, MJNameKey);
}//把变量的地址值付给自己
static const void *MJNameKey = &MJNameKey;
- (void)setName:(NSString *)name
{objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}- (NSString *)name
{return objc_getAssociatedObject(self, MJNameKey);
}//直接定义char类型的变量 不用赋值。直接使用变量的地址值static const char MJNameKey;
- (void)setName:(NSString *)name
{objc_setAssociatedObject(self, &MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}- (NSString *)name
{return objc_getAssociatedObject(self, &MJNameKey);
}//直接使用方法对象作为key
- (void)setName:(NSString *)name
{objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}- (NSString *)name
{// 隐式参数// _cmd == @selector(name)return objc_getAssociatedObject(self, _cmd);
}
//- (NSString *)name
//{
// return objc_getAssociatedObject(self, @selector(name));
//}
4.2 关联对象的底层数据结构
- 实现关联对象的技术的核心对象有:
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
- 源码
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {_object_set_associative_reference(object, (void *)key, value, policy);
}void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {// retain the new value (if any) outside the lock.ObjcAssociation old_association(0, nil);id new_value = value ? acquireValue(value, policy) : nil;{AssociationsManager manager;AssociationsHashMap &associations(manager.associations());disguised_ptr_t disguised_object = DISGUISE(object);if (new_value) {// break any existing association.AssociationsHashMap::iterator i = associations.find(disguised_object);if (i != associations.end()) {// secondary table existsObjectAssociationMap *refs = i->second;ObjectAssociationMap::iterator j = refs->find(key);if (j != refs->end()) {old_association = j->second;j->second = ObjcAssociation(policy, new_value);} else {(*refs)[key] = ObjcAssociation(policy, new_value);}} else {// create the new association (first time).ObjectAssociationMap *refs = new ObjectAssociationMap;associations[disguised_object] = refs;(*refs)[key] = ObjcAssociation(policy, new_value);object->setHasAssociatedObjects();}} else {// setting the association to nil breaks the association.AssociationsHashMap::iterator i = associations.find(disguised_object);if (i != associations.end()) {ObjectAssociationMap *refs = i->second;ObjectAssociationMap::iterator j = refs->find(key);if (j != refs->end()) {old_association = j->second;refs->erase(j);}}}}// release the old value (outside of the lock).if (old_association.hasValue()) ReleaseValue()(old_association);
}
关联对象流程分析图:
- 总结:
1. 关联对象并不是存储在被关联对象本省内存中
2. 关联对象存储在全局统一的AssociationsManager中
3. 设置关联对象为nil,就相当于是移除关联对象
4. 关联的对象如果被销毁了,那么缓存这个对象的所有属性会被移除
5. 面试题
5.1 Category的使用场合是什么?
- 把比较复杂的类拆解的时候就用到分类
5.2 Category的实现原理?
- Category编译之后的底层结构是 struct _category_t,里面储存着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将Category的数据后,合并到类信息中(类对象、元类对象中)
5.3 Category和 Class Extension的区别是什么?
- Calss Exyension在编译的时候,它的数据就已近包含在类信息中(类扩展- 在编译的时候,就把扩展中的属性、方法合并到类中的.m文件中,变成私有的)
- Category是在运行中才会将数据合并到类信息中
5.4 Category中又load方法吗?load方法是在什么时候调用的?load方法能继承吗?
- category中有load方法
- load方法是在runtime加载类、分类的时候调用
- laod方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用(子类调用父类的+load方法是按照消息机制来调用)
- 子类调用父类的+laod方法,肯定是先调用父类分类的+load方法的(因为手动调用父类的+load方法是是通过消息机制调用的,而前面我们说过。调用方法的时候,分类的方法运行的时候是放在类的方法前面的(相同的方法)而消息机制是通过isa指针找到meta_class对象,在去寻找方法列表,遍历方法列表,找到方法调用)
5.5 load、initialize方法的区别是什么?它们在category中的调用顺序?以及出现继承他们之间的调用过程?
-
区别:
- 调用方式区别:
- load是直接根据函数地址直接调用
- initialize是通过objc_msgSend调用(方式的不同导致后面很多调用结果不同)
- 调用时刻不同:
- load是runtime加载类、分类的时候调用,只会调用一次
- initialize是类第一次接受到消息的时候调用,每个类只会initialize一次(但是父类的initialize可能会被调用多次)
- 调用方式区别:
-
调用顺序:
- load是先调用类的load(先编译的类,优先调用load,调用子类的load之前,会先调用父类的load方法),其次在次调用分类的load方法,先编译的分类,优先调用load方法
- initialize 先初始化父类,在初始化子类(可能最终调用的是父类的方法)
5.6 Category能否添加成员变量?如果可以,如何给Category添加成员变量?
- 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
- 使用Runtime来把需要添加的属性和对象关联起来