简介 Category
是 Objective-C 2.0
之后添加的特性,一般我们使用 Category
的场景主要可以动态地为已经存在的类扩展新的属性和方法。这样做的好处就是:
可以减少臃肿的代码。
可以把不同的功能拆开,方便以后的维护。
以下源码来自于opensource.apple.com 的objc4-750.tar.gz 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 Presen类 // Presen.h #import <Foundation/Foundation.h> @interface Preson : NSObject { int _age; } - (void)run; @end // Presen.m #import "Preson.h" @implementation Preson - (void)run { NSLog(@"Person - run"); } @end Presen扩展1 // Presen+Test.h #import "Preson.h" @interface Preson (Test) <NSCopying> - (void)test; + (void)abc; @property (assign, nonatomic) int age; - (void)setAge:(int)age; - (int)age; @end // Presen+Test.m #import "Preson+Test.h" @implementation Preson (Test) - (void)test { } + (void)abc { } - (void)setAge:(int)age { } - (int)age { return 10; } @end Presen分类2 // Preson+Test2.h #import "Preson.h" @interface Preson (Test2) @end // Preson+Test2.m #import "Preson+Test2.h" @implementation Preson (Test2) - (void)run { NSLog(@"Person (Test2) - run"); } @end
实例对象的 isa
指针指向类对象,类对象的 isa
指针指向元类对象,当 Preson
对象调用 run
方法时,通过实例对象的 isa
指针找到类对象,然后在类对象中查找对象方法,如果没有找到,就通过类对象的 superclass
指针找到父类对象,接着去寻找 run
方法。
猜想结论:通过 runtime 机制动态将分类中的对象方法存储在类对象中的;将分类中类方法的话,存储在元类对象中。
数据结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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); };
注意:
name
:是指 class_name
而不是 category_name
。
cls
:要扩展的类对象,编译期间是不会定义的,而是在运行时通过 * name
对应到对应的类对象。
instanceMethods
:category 中所有给类添加的实例方法的列表。
classMethods
:category 中所有添加的类方法的列表。
protocols
:category 实现的所有协议的列表。
instanceProperties
:category 中添加的所有属性。
发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成 get
和 set
方法的声明,需要自己去实现。
从 category 的定义也可以看出 category 可以添加实例方法,类方法,甚至可以实现协议,添加属性,无法添加实例变量。
For example 使用 clang
的命令去看看 category
到底会变成什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 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; }; extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache; #pragma warning(disable:4273) static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[1]; } _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_MJPerson_Test_test}} }; static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[1]; } _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_MJPerson_Test_test2}} }; extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_MJPerson; static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { "MJPerson", 0, // &OBJC_CLASS_$_MJPerson, (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test, (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test, 0, 0, }; static void OBJC_CATEGORY_SETUP_$_MJPerson_$_Test(void ) { _OBJC_$_CATEGORY_MJPerson_$_Test.cls = &OBJC_CLASS_$_MJPerson; } #pragma section(".objc_inithooks$B", long, read, write) __declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = { (void *)&OBJC_CATEGORY_SETUP_$_MJPerson_$_Test, }; static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= { &_OBJC_$_CATEGORY_MJPerson_$_Test, };
从代码中可以发现:
编译器生成了 _OBJC_$_CATEGORY_MJPerson_$_Test
的结构体,_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test
对象方法,_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test
类方法,并且一一对应为上面结构体内赋值。 并且两者的命名格式都遵循了公共前缀+扩展列表+类名+category 名字的命名方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[1]; } _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_MJPerson_Test_test}} }; static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[1]; } _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_MJPerson_Test_test2}} };
从 _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test
和 _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test
结构体中可以找到分类中对应的对象方法和类方法。
_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test
结构体中名称用 static
来修饰,目的是用来区分可以在不同分类中扩展各种列表以及后面的 category
结构体本身命名,所以在同一个编译单元里我们的 category
名不能重复,否则会出现编译错误。
Category 的加载处理过程
通过 Runtime
加载某个类的所有 Category
数据
把所有 Category
的方法、属性、协议数据,合并到一个大数组中 后面参与编译的 Category
数据,会在数组的前面
将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
Category
是依赖于 dyld
动态加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void _objc_init(void) { static bool initialized = false; if (initialized) return; initialized = true; // fixme defer initialization until an objc-using image is found? environ_init(); tls_init(); lock_init(); exception_init(); // Register for unmap first, in case some +load unmaps something _dyld_register_func_for_remove_image(&unmap_image); dyld_register_image_state_change_handler(dyld_image_state_bound, 1/*batch*/, &map_images); dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images); }
Category
被附加到类上面是在 map_images
的时候发生的,在 new-ABI
的标准下,_objc_init
里面的调用的 map_images
最终会调用 objc-runtime-new.mm
里面的 _read_images
方法,而在 _read_images
方法的结尾,有以下的代码片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 /*********************************************************************** * _read_images * Perform initial processing of the headers in the linked * list beginning with headerList. * * Called by: map_images_nolock * * Locking: runtimeLock acquired by map_images **********************************************************************/ // 读取镜像 void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) { if (DisableTaggedPointers) { disableTaggedPointers(); } initializeTaggedPointerObfuscator(); if (PrintConnecting) { _objc_inform("CLASS: found %d classes during launch", totalClasses); } // namedClasses // Preoptimized classes don't go in this table. // 4/3 is NXMapTable's load factor int namedClassesSize = (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3; gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize); allocatedClasses = NXCreateHashTable(NXPtrPrototype, 0, nil); ts.log("IMAGE TIMES: first time tasks"); } // Discover categories. for (EACH_HEADER) { 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; if (cat->instanceMethods || cat->protocols || cat->instanceProperties) { addUnattachedCategoryForClass(cat, cls, hi); if (cls->isRealized()) { // 重新组织类对象 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()) { // 重新组织元类对象 remethodizeClass(cls->ISA()); } if (PrintConnecting) { _objc_inform("CLASS: found category +%s(%s)", cls->nameForLogging(), cat->name); } } } } }
从代码中看出, 通过 _getObjc2ClassList
方法获取所有的 Category
然后添加到 catlist
数组中;把 Category
的实例方法、协议以及属性添加到类上;把 Category
的类方法和协议添加到类的 metaclass
上。addUnattachedCategoryForClass
方法只是把类和 Category
做一个关联映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static void remethodizeClass(Class cls) { category_list *cats; bool isMeta; runtimeLock.assertLocked(); isMeta = cls->isMetaClass(); // Re-methodizing: check for more categories if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { if (PrintConnecting) { _objc_inform("CLASS: attaching categories to class '%s' %s", cls->nameForLogging(), isMeta ? "(meta)" : ""); } // 把类对象或者元类对象的分类信息附加到原来的类里面 attachCategories(cls, cats, true /*flush caches*/); free(cats); } }
remethodizeClass
方法将 Category
合并到类中,然后更新类的方法列表、协议列表和属性列表,更新类以及子类的方法缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 static void attachCategories(Class cls, category_list *cats, bool flush_caches) { if (!cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); // 根据 isMeta 判断是类对象还是元类对象 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 first int mcount = 0; int propcount = 0; int protocount = 0; int i = cats->count; bool fromBundle = NO; while (i--) { // 取出某一个分类 auto& entry = cats->list[i]; // 将分类中的方法存入 mlist 数组中 method_list_t *mlist = entry.cat->methodsForMeta(isMeta); if (mlist) { mlists[mcount++] = mlist; fromBundle |= entry.hi->isBundle(); } // 将分类中的属性方法存入 mlist 数组中 property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi); if (proplist) { proplists[propcount++] = proplist; } // 将分类中的协议存入 mlist 数组中 protocol_list_t *protolist = entry.cat->protocols; if (protolist) { protolists[protocount++] = protolist; } } // class_rw 结构体,用来存储类对象的对象方法,属性方法和协议 auto rw = cls->data(); prepareMethodLists(cls, mlists, mcount, NO, fromBundle); // 将所有分类的对象方法列表附加到类对象的方法列表中 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); }
attachCategories
方法中,将方法列表、属性和协议列表从 Category
合并到类中。 综上所述: 首先根据方法列表,属性列表,协议列表,malloc
分配内存,根据多少个分类以及每一块方法需要多少内存来分配相应的内存地址。
通过 while
循环,遍历所有的 Category
,得到它的方法列表 mlist
、proplist
和 protolist
并存入 mlists
、proplists
和 protolists
中。换句话说,它的主要作用就是将 Category
中的方法、属性和协议拼接到类(主类或元类)中,更新类的数据字段 data()
中 mlist
、proplist
和 protolist
的值。
之后通过类对象的 data()
方法,拿到类对象的 class_rw_t
结构体 rw
,调用方法列表、属性列表、协议列表的 attachList
函数,将所有的分类的方法、属性、协议列表数组传进去,将分类和本类相应的对象方法,属性,和协议进行了合并。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; if (hasArray()) { // many lists -> many lists uint32_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 所有分类的方法列表,属性列表,协议列表 memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } else if (!list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1 : 0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } }
attachList
函数中 memmove
内存移动和 memcpy
内存拷贝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // memmove :内存移动。 /* __dst : 移动内存的目的地 * __src : 被移动的内存首地址 * __len : 被移动的内存长度 * 将__src的内存移动__len块内存到__dst中 */ void *memmove(void *__dst, const void *__src, size_t __len); // memcpy :内存拷贝。 /* __dst : 拷贝内存的拷贝目的地 * __src : 被拷贝的内存首地址 * __n : 被移动的内存长度 * 将__src的内存移动__n块内存到__dst中 */ void *memcpy(void *__dst, const void *__src, size_t __n);
经过 memmove
和 memcpy
方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面。为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用,本类的方法依然在内存中的。
通过以上可以看出:
Category
的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 Category
和原来类都有 methodA
,那么 category
附加完成之后,类的方法列表里会有两个 methodA
。
Category
的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 Category
的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。
分类的加载顺序取决于编译的顺序,最后面参与编译的分类优先调用。
Category和Class Extension的区别是什么 Class Extension
在编译的时候,它的数据就已经包含在类信息中。Category
是在运行时,才会将数据合并到类信息中。