牛叉的demoiOS技术点iOS 开发每天分享优质文章

Category中方法的优先级为什么会高于主类(或父类)

2017-11-23  本文已影响95人  iOS俱哥

在这里先说下结论:在执行方法时,子类的优先级高于父类,分类的优先级高于主类。

在很多的情况下,往往是给系统自带的类添加分类,如NSObject和NSString,因为有的时候,系统类可能并不能满足我们的要求。在这里只是为了测试方便,对子类添加了分类。

开始测试:

一:先建立一个父类Animal类

.h文件:

#import <Foundation/Foundation.h>
@interface Animal : NSObject
@property(copy,nonatomic) NSString * name;//一个name属性
@end

.m文件:

#import "Animal.h"

@implementation Animal
- (NSString *)name{
    NSLog(@"%s",__func__);//用户打印方法,测试是否执行
    return @"小动物";
}
@end

二:建一个继承与Animal类的Cat类

.h文件:

#import "Animal.h"

@interface Cat : Animal

@end

.m文件:

#import "Cat.h"

@implementation Cat
- (NSString *)name{
    NSLog(@"%s",__func__);
    return @"小猫";
}
@end

三:建一个Cat的分类

.h文件:

#import "Cat.h"

@interface Cat (Black)

@end

.m文件:

@implementation Cat (Black)
- (NSString *)name{
    NSLog(@"%s",__func__);
    return @"小黑猫";
}
@end

以上是建立几个测试类的过程,接下来是验证的过程。

四:实例化类对象

1.验证分类在执行方法时,优先级高于主类。

在一个VC中代码如下:

   - (void)viewDidLoad {
    [super viewDidLoad];
    
    Cat * cat = [[Cat alloc]init];
    
    NSLog(@"cat name is %@",cat.name);
}

打印结果如下:


图一.png

2.验证子类在执行方法时,优先级高于父类。

把分类Cat+Black.m代码修改(注释掉)如下:

#import "Cat+Black.h"

@implementation Cat (Black)
//- (NSString *)name{
//    NSLog(@"%s",__func__);
//    return @"小黑猫";
//}
@end

在运行打印结果如下:

图二.png
这也证明了如果想要父类方法执行的时候写[Super ...]了。

3.验证父类的方法到底是否会执行。

再把子类Cat.m代码修改(注释掉)如下:

#import "Cat.h"

@implementation Cat
//- (NSString *)name{
//    NSLog(@"%s",__func__);
//    return @"小猫";
//}
@end

再运行看打印结果:

图三.png
打印结果说明,父类的方法会被执行,只是在没有子类重写的情况下。
那么问题来了:在执行方法时,子类的优先级高于父类,分类的优先级高于主类。为什么会出现这样的情况呢?
这要归结为Objective-C的Runtime的消息机制,具体解释如下:
我在例子中,取name属性用的是点语法,实际等同于[cat name]这个get方法。

下面我们就来看看具体消息发送之后是怎么来动态查找对应的方法的。

首先,编译器将代码[obj name];转化为objc_msgSend(obj, @selector (name));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中 通过SEL查找对应函数method,若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。

上边的解释已经说得很清楚了,在查找name方法时,先从子类开始查找,若找到了就直接执行不再向父类查找,若没有找到,才会向父类中查找。

但是还有一个问题,分类的优先级为什么会高于主类呢?

接着分析:结合 runtime(我下载的是当前的最新版本 objc4-723.tar.gz) 的源码来看一下是怎么回事。

我们知道,无论我们有没有主动引入 Category 的头文件,Category 中的方法都会被添加进主类中。我们可以通过- performSelector: 等方式对 Category 中的相应方法进行调用,之所以需要在调用的地方引入 Category 的头文件,只是为了"照顾"编译器的感受。

下面,我们将结合 runtime 的源码探究下 Category 的实现原理。打开 runtime 源码工程,在文件 objc-runtime-new.mm 中找到以下函数:

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
...
 // 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);
                }
            }
        }
    }

    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.
...
}

从以上代码中,我们可以知道在这个函数中对 Category 做了如下处理:

1.将 Category 和它的主类(或元类)注册到哈希表中;
2.如果主类(或元类)已实现,那么重建它的方法列表。

我们注意到,不管是哪种情况,最终都是通过调用static void remethodizeClass(Class cls)函数来重新整理类的数据的。

static void remethodizeClass(Class cls)
{
    ...
                         cls->nameForLogging(), 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);
        }
        cls->data()->protocols = newprotos;

        _free_internal(cats);
    }
}

这个函数的主要作用是将 Category 中的方法、属性和协议整合到类(主类或元类)中,更新类的数据字段 data()method_lists(或 method_list)、propertiesprotocols的值。进一步,我们通过 attachCategoryMethods函数的源码可以找到真正处理 Category 方法的 attachMethodLists函数:

static void
attachMethodLists(Class cls, method_list_t **addedLists, int addedCount,
                  bool baseMethods, bool methodsFromBundle,
                  bool flushCaches)
{
    ...
        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;

    if (newCount > 1) {
        assert(newLists != newBuf);
        cls->data()->method_lists = newLists;
        cls->setInfo(RW_METHOD_ARRAY);
    } else {
        assert(newLists == newBuf);
        cls->data()->method_list = newLists[0];
        assert(!(cls->data()->flags & RW_METHOD_ARRAY));
    }
}

上边的代码的主要作用就是将类中的旧有方法和 Category 中新添加的方法整合成一个新的方法列表,并赋值给method_listsmethod_list。通过探究这个处理过程,我们也印证了一个结论,那就是主类中的方法和 Category 中的方法在 runtime 看来并没有区别,它们是被同等对待的,都保存在主类的方法列表中。

特别注意的是:不要用Category来覆写系统类的方法,可能会造成非常严重的后果

参考文章:
Objective-C总Runtime的那点事儿(一)消息机制
【iOS】运行时消息传递与转发机制
Objective-C Category 的实现原理

上一篇 下一篇

猜你喜欢

热点阅读