以下是在看完源码后我觉得最能说明问题的部分。
首先是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 方法。