当前位置: 代码迷 >> 综合 >> OC底层学习-Category
  详细解决方案

OC底层学习-Category

热度:30   发布时间:2024-02-09 19:59:36.0

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源码分析

  • 结论:

    1. Category(分类)中的所有对象方法最终都会放在class对象的方法列表中的,调用Category中的方法,最终都会通过instance的isa指针找到class对象,然后找到方法并执行
    2. Category(分类)中所有的类(+)方法最后都会放在meta-class对象的方法列表中,类方法的调用过程,最后会通过isa的指正找到class对象,在通过class的isa指正找到meta-class对象中,然后寻找类方法执行
    3. 分类的数据是在程序运行的过程中把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的加载过程和一些注意点

  1. Category在编译期还没有合并到某个类的数据中的, 是在运行时通过Runtime加载某个类的所有Category数据
  2. 在运行时,把所有Category的方法、属性、协议数据,合并到一个大数组中。过程如下
    • 首先将原来类对象的方法列表进行一个扩容,然后把原来的方法列表挪到后面
    • 在把分类中的方法列表添加到类对象的方法列表中, 最后就是分类中所有的方法都添加到类对象的方法列表中
    • 注意:
      • category和类中都存在同一个方法的时候,会优先调用category中的方法(由上面源码分析可知)
      • 当多个分类都存在同一个方法时,优先调用哪个分类中的方法,取决于category的编译顺序,会优先加载最后编译的category,越是最后编译的category,方法列表等数据信息越在前(由上面源码分析可知)
      • 分类中的方法并没有覆盖原来类中的方法,只是方法的执行顺序超前了,因为所有的方法最终都会合并到类对象的方法列表中,而方法执行,只需要找到方法的执行就不会往后面继续找
  3. 将合并后的分类数据(方法、属性、协议)插入到类原来的数据前面

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方法之前
在这里插入图片描述

  1. +load方法会在runtime加载类,分类的时候调用
  2. 每个类、分类的+laod,在程序运行过程中只调用一次
  3. +load方法调用顺序:
    • 首先调类的+laod方法
    • 按照变异顺序调用(先编译,先调用)
    • 调用子类的+load方法之前会调用父类的+load方法
    • 调用完类的+load方法之后,在调用分类的+load方法
    • 分类的+load方法顺序也是按照编译顺序调用(先编译,先调用)
  4. 调用+load方法底层是直接通过指针(地址)直接调用的+load方法(直接通过在内存中的地址直接去调用),而调用分类的类方法,本质上是给对象发送一个消息(也就是通过objc_msgSend()),但是通过这种模式(消息机制),它的调用过程是首先通过isa指针找到meta-class对象,然后在去遍历方法列表中的方法,找到方法之后再调用,调用之后接结束不在往下寻找
  5. +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方法

  1. +initialize()方法会在的第一次接收到消息时调用。

  2. 调用顺序:

    • 先调用父类的+initialize方法,在调用子类的+initialize方法(子类内部会主动调用父类的+initialize(),前提是父类没有初始化)
    • 先初始化父类,在初始化子类,每个类只会初始化一次
  3. +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()方法使用场景:
    1. +initialize():需要在类第一次被调用做些事情的时候,可在方法内部做些事情
    2. +load(): 类被加载内存中的时候需要做事情的时候,可在方法内部做些事情

4. 关联对象

4.1关联对象的概述和简单运用

  1. 在类中声明 @property (nonatomic, assign) int age;做了什么事情:
    • 创建一个_age的成员变量
    • 生成一个set方法和get方法的声明
    • 生成set和get方法的实现
  2. 在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
  • 定义一个全局的字典来储存,实现对象和睡醒一对一的关系,这种方法可以实现,也可行,但是还是存在一些问题:
    1. 如果你的需求不是要求这个类一直存在, 那么就会存在内存泄漏的问题
    2. 线程安全的问题
    3. 过程比较麻烦,没增加一个属性都需要对象增加一个全局的字典来储存
@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的用法:
    1. 定义一个指针变量key const void *key;如果没有初始化的可能造成共用一个属性值,可以static const void * key = &key(如果不用static修改限定变量的范围,那别人可以在外部使用extern字段用用字段,然后在外面修改这个值)
    2. 定义char类型的变量static const char KEY因为char类型的变量只占一个字节,相比于直接定义指针类型的变量(占8个字节)节省内存,不需要赋值,因为传递进去的需要的变量的地址值,而不是变量的值
    3. 或则key的值直接传递@“key”,因为直接写一个字符串,这个字符串是放在常量区的,是不会变的,因为我们字符串声明就NSString *p = @"key";
    4. 直接传递一个方法对象@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 关联对象的底层数据结构

  1. 实现关联对象的技术的核心对象有:
    • 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的实现原理?

  1. Category编译之后的底层结构是 struct _category_t,里面储存着分类的对象方法、类方法、属性、协议信息
  2. 在程序运行的时候,runtime会将Category的数据后,合并到类信息中(类对象、元类对象中)

5.3 Category和 Class Extension的区别是什么?

  1. Calss Exyension在编译的时候,它的数据就已近包含在类信息中(类扩展- 在编译的时候,就把扩展中的属性、方法合并到类中的.m文件中,变成私有的)
  2. 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中的调用顺序?以及出现继承他们之间的调用过程?

  1. 区别:

    • 调用方式区别:
      • load是直接根据函数地址直接调用
      • initialize是通过objc_msgSend调用(方式的不同导致后面很多调用结果不同)
    • 调用时刻不同:
      • load是runtime加载类、分类的时候调用,只会调用一次
      • initialize是类第一次接受到消息的时候调用,每个类只会initialize一次(但是父类的initialize可能会被调用多次)
  2. 调用顺序:

    • load是先调用类的load(先编译的类,优先调用load,调用子类的load之前,会先调用父类的load方法),其次在次调用分类的load方法,先编译的分类,优先调用load方法
    • initialize 先初始化父类,在初始化子类(可能最终调用的是父类的方法)

5.6 Category能否添加成员变量?如果可以,如何给Category添加成员变量?

  1. 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
  2. 使用Runtime来把需要添加的属性和对象关联起来
  相关解决方案