以下是在看完源码后我觉得最能说明问题的部分。
首先是category结构体的定义:
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的定义也可以看出category可以添加实例方法,类方法;可以遵守协议,添加属性;但无法添加实例变量。注意,在category中可以有属性(property),但是该属性只是生成了getter和setter方法的声明,并没有产生对应的实现,更不会添加对应的实例变量。如果想为实例对象添加实例变量,可以尝试使用关联引用技术。
下面的方法初步整合了上述可以增加的选项。
static void remethodizeClass(Class cls) { category_list *cats;//分类列表 BOOL isMeta;//元类标识 //加锁 rwlock_assert_writing(&runtimeLock); isMeta = cls->isMetaClass();//获得元类的标识 // Re-methodizing: check for more categories //如果该类有尚未添加的分类 if ((cats = unattachedCategoriesForClass(cls))) { chained_property_list *newproperties;//指向属性的指针 const protocol_list_t **newprotos;//指向协议的二级指针 //输出相关信息 if (PrintConnecting) { _objc_inform("CLASS: attaching categories to class '%s' %s", cls->name(), isMeta ? "(meta)" : ""); } // Update methods, properties, protocols attachCategoryMethods(cls, cats, YES);//函数的添加相对复杂,在下面说明 //非元类时,返回分类中的属性列表 newproperties = buildPropertyList(nil, cats, isMeta); if (newproperties) { //将原来类中的属性列表添加在分类属性列表之后 newproperties->next = cls->data()->properties; cls->data()->properties = newproperties;//更新属性列表 } //根据当前类和分类中的信息构新的建协议列表 newprotos = buildProtocolList(cats, nil, cls->data()->protocols); //若原本协议列表不为空,且发生了更改 if (cls->data()->protocols && cls->data()->protocols != newprotos) { _free_internal(cls->data()->protocols);//释放存放原来protocol的内存 } cls->data()->protocols = newprotos;//重新设置协议列表 _free_internal(cats);//释放分类占用的内存 } }
最终处理函数的方法是:
static void attachMethodLists(Class cls, method_list_t **addedLists, intaddedCount, bool baseMethods, bool methodsFromBundle, bool flushCaches);
该函数较长,主要操作如下:
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] = nil; oldLists = oldBuf; } if (oldLists) {//原本有方法列表的情况下计算方法列表的个数 //每一个oldLists[i]都是一个方法列表 while (oldLists[oldCount]) oldCount++; } int newCount = oldCount; for (int i = 0; i < addedCount; i++) { if (addedLists[i]) newCount++; // only non-nil entries get added }//计算完成后newCount中的值是添加上category中的方法列表后的方法列表总值。 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; }//重新分配方法列表数组的内存 // Add method lists to array. // Reallocate un-fixed method lists. // The new methods are PREPENDED to the method list array. newCount = 0; int i; for (i = 0; i < addedCount; i++) { method_list_t *mlist = addedLists[i]; if (!mlist) continue;//如果mlist为空,继续搜索下一个 // Fixup selectors if necessary if (!isMethodListFixedUp(mlist)) { mlist = fixupMethodList(mlist, methodsFromBundle, true/*sort*/); } // Scan for method implementations tracked by the class's flags //根据类中的标志来寻找方法的实现 for (uint32_t m = 0; (scanForCustomRR || scanForCustomAWZ) && m < mlist->count; m++) { SEL sel = method_list_nth(mlist, m)->name; if (scanForCustomRR && isRRSelector(sel)) { cls->setHasCustomRR(); scanForCustomRR = false; } else if (scanForCustomAWZ && isAWZSelector(sel)) { cls->setHasCustomAWZ(); scanForCustomAWZ = false; } } // Update method caches //有需要的话,更新方法的缓存 if (flushCaches) { cache_eraseMethods(cls, mlist); } // Fill method list array //逐个将新添加的方法列表加入新建的二级列表中 newLists[newCount++] = mlist; } // Copy old methods to the method list array //将原本有的方法列表添加在后面 for (i = 0; i < oldCount; i++) { newLists[newCount++] = oldLists[i]; } //原本有函数列表,则释放相应的内存 if (oldLists && oldLists != oldBuf) free(oldLists); // nil-terminate newLists[newCount] = nil;
由上面的代码可知,方法列表的添加过程如下图所示(假设添加时都只有一组方法列表):
添加方法列表的时候是后添加的在新形成的列表前部,这也是为什么在有多个category中有同名方法时,后编译的在调用时会“覆盖”前面已编译的方法。其实方法本身并没有被覆盖,只是调用的时候是从上而下查找方法列表,当运行时找到对应的方法名后就去忙着调用了,并不会管后面的同名方法。
关于load方法:
其实在上面的源码中也可以看到, category 中对load方法的处理过并没有什么特殊。因此,可以说category 中的 load 方法跟普通方法一样也会对主类中的 load方法造成覆盖,只不过 runtime在自动调用主类和 category中的 load方法时是直接使用各自方法的指针进行调用的。所以我们感觉不到category对主类的影响。其实手动给主类发送load 消息时,调用的将会是分类中的load 方法。