Objective-C Category(分类)之于我而言有种神秘感,虽然自己已经在实际开发过程中已经多次使用它,且感受到了它带来的便利与高效。但是我却仅仅是停留在对它的基本使用层面,除此之外一无所知。我能感觉它的强大,心中也一直有种对它内部实现一探究竟的冲动,奈何迟迟没有行动。时间愈久,这种情绪愈发浓烈,今天终究是按耐不住了…
知其然
对于苹果应用开发者来说,开发者想要快速地了解或是回顾某个知识点,Apple开发者文档往往是不二首选。
文档上如是说:你可以使用Category为一个已经存在的类添加额外的方法,比如Cocoa库中的类,即便是这个类的源代码是不可见的-不能子类化。使用Category给类添加的方法能被其子类继承,且在Runtime下其与类原有的方法是无差别的。
分类的使用场景:
- 在不改变某个类源文件和不使用继承的前提下,为该类添加先的方法
- 声明类的私有方法
- 将一个类的实现拆分为多个独立的源文件
很明显,Category其实就是设计模式之一的装饰者模式的具体实现。
注意,Category是一个类的拓展,为不是一个新类。
借助Apple开发者文档了解到Category的“知其然”,然后就是基于Apple Opensource来解决“知其所以然”的问题?
知其然所以然
此处使用的源码版本是objc4-532.2。与本文相关的代码都在源文件objc-runtime-new.mm中,接下来就结合关键的代码与注释进行分析。
Catrgory的定义
1 2 3 4 5 6 7 8 9 10
| typedef 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; } category_t;
|
通过Category的定义可以看出,Category与Class存在很相似。不过Category没有isa指针,这也说明Category不是一个类,只能作为一个类的拓展存在。
关键Method-1: _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
| void _read_images(header_info **hList, uint32_t hCount) { ... #define EACH_HEADER \ hIndex = 0; \ crashlog_header_name(NULL) && hIndex < hCount && (hi = hList[hIndex]) && crashlog_header_name(hi); \ hIndex++ ... for (EACH_HEADER) { category_t **catlist = _getObjc2CategoryList(hi, &count); for (i = 0; i < count; i++) { category_t *cat = catlist[i]; class_t *cls = remapClass(cat->cls); if (!cls) { catlist[i] = NULL; if (PrintConnecting) { _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with " "missing weak-linked target class", cat->name, cat); } continue; } BOOL classExists = NO; if (cat->instanceMethods || cat->protocols || cat->instanceProperties) { addUnattachedCategoryForClass(cat, cls, hi); if (isRealized(cls)) { remethodizeClass(cls); classExists = YES; } if (PrintConnecting) { _objc_inform("CLASS: found category -%s(%s) %s", getName(cls), cat->name, classExists ? "on existing class" : ""); } } if (cat->classMethods || cat->protocols ) { addUnattachedCategoryForClass(cat, cls->isa, hi); if (isRealized(cls->isa)) { remethodizeClass(cls->isa); } if (PrintConnecting) { _objc_inform("CLASS: found category +%s(%s)", getName(cls), cat->name); } } } } #undef EACH_HEADER }
|
_read_images()是赋值读取镜像文件的函数,函数末尾就是处理Category的代码块。其中将工程中所有的Category分别与其目标类建立关联,然后调用了remethodizeClass()对目标类的进行重构。
关键Method-2: remethodizeClass()
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
| static void remethodizeClass(class_t *cls) { category_list *cats; BOOL isMeta; rwlock_assert_writing(&runtimeLock); isMeta = isMetaClass(cls); if ((cats = unattachedCategoriesForClass(cls))) { chained_property_list *newproperties; const protocol_list_t **newprotos; if (PrintConnecting) { _objc_inform("CLASS: attaching categories to class '%s' %s", getName(cls), isMeta ? "(meta)" : ""); } BOOL vtableAffected = NO; attachCategoryMethods(cls, cats, &vtableAffected); newproperties = buildPropertyList(NULL, cats, isMeta); if (newproperties) { newproperties->next = cls->data()->properties; cls->data()->properties = newproperties; } newprotos = buildProtocolList(cats, NULL, cls->data()->protocols); if (cls->data()->protocols && cls->data()->protocols != newprotos) { _free_internal(cls->data()->protocols); } cls->data()->protocols = newprotos; _free_internal(cats); flushCaches(cls); if (vtableAffected) flushVtables(cls); } }
|
remethodizeClass()函数的功能比较简单,进一步细化了对Category中的方法列表、协议列表和属性列表的处理。其中,属性列表的处理则是直接插入原属性链表头部,协议列表则是附加到原协议列表的尾部。接下来,重点分析处理Category方法列表的attachCategoryMethods函数。
关键Method-3: attachCategoryMethods()
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
| static void attachCategoryMethods(class_t *cls, category_list *cats, BOOL *inoutVtablesAffected) { if (!cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); BOOL isMeta = isMetaClass(cls); method_list_t **mlists = (method_list_t **) _malloc_internal(cats->count * sizeof(*mlists)); int mcount = 0; int i = cats->count; BOOL fromBundle = NO; while (i--) { method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta); if (mlist) { mlists[mcount++] = mlist; fromBundle |= cats->list[i].fromBundle; } } attachMethodLists(cls, mlists, mcount, NO, fromBundle, inoutVtablesAffected); _free_internal(mlists); }
|
attachCategoryMethods()函数的功能也比较简单,对与目标类的Category中所有方法进行汇总,然后调用attachMethodLists函数进行处理。
关键Method-4: attachMethodLists()
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
| static void attachMethodLists(class_t *cls, method_list_t **addedLists, int addedCount, BOOL baseMethods, BOOL methodsFromBundle, BOOL *inoutVtablesAffected) { rwlock_assert_writing(&runtimeLock); ... method_list_t *oldBuf[2]; method_list_t **oldLists; int oldCount = 0; if (cls->data()->flags & RW_METHOD_ARRAY) { oldLists = cls->data()->method_lists; } else { oldBuf[0] = cls->data()->method_list; oldBuf[1] = NULL; oldLists = oldBuf; } if (oldLists) { while (oldLists[oldCount]) oldCount++; } int newCount = oldCount; for (int i = 0; i < addedCount; i++) { if (addedLists[i]) newCount++; } method_list_t *newBuf[2]; method_list_t **newLists; if (newCount > 1) { newLists = (method_list_t **) _malloc_internal((1 + newCount) * sizeof(*newLists)); } else { newLists = newBuf; } newCount = 0; int i; for (i = 0; i < addedCount; i++) { method_list_t *mlist = addedLists[i]; if (!mlist) continue; if (!isMethodListFixedUp(mlist)) { mlist = fixupMethodList(mlist, methodsFromBundle, true); } ... newLists[newCount++] = mlist; } for (i = 0; i < oldCount; i++) { newLists[newCount++] = oldLists[i]; } if (oldLists && oldLists != oldBuf) free(oldLists); newLists[newCount] = NULL; if (newCount > 1) { assert(newLists != newBuf); cls->data()->method_lists = newLists; changeInfo(cls, RW_METHOD_ARRAY, 0); } else { assert(newLists == newBuf); cls->data()->method_list = newLists[0]; assert(!(cls->data()->flags & RW_METHOD_ARRAY)); } }
|
attachMethodLists才是最关键的函数。函数中为目标类分配了一个新的函数列表,先加入Category中的方法,再加入目标类原有方法。这也就是为什么如果Category中的函数与目标类中的函数重名,那么目标类的函数会被覆盖的原因。因为Runtime在遍历方法列表时会先发现Category中的函数。
小结
这篇博客基于源代码对Category与目标类的组合过程进行了分析,明白了Category中的方法、协议和属性的处理流程。因此,我们可以更加高效和准确地使用Category,甚至利用其中存在的“漏洞”实现一些小魔法。