美文网首页
iOS方法调用覆盖分析

iOS方法调用覆盖分析

作者: jayhe | 来源:发表于2020-03-14 16:12 被阅读0次

    先看看几个示例

    1. 我们在类中定义了2个属性,name、Name看看使用KVC方法设置值的时候有什么问题

    @interface TestMethodInvocation ()
    
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, copy) NSString *Name;
    
    @end
    
    @implementation TestMethodInvocation
    
    - (void)testKVCSetProperty {
        [self setValue:@"JayHe" forKey:@"Name"];
        NSLog(@"Name = %@ name = %@", self.Name, self.name);
        // 输出结果:RuntimeLearning[24880:398327] Name = (null) name = JayHe
    }
    
    @end
    
    • 发现输出结果不大符合预期,明明我们是想给Name设置值,结果发现设置给了name
      我们知道KVC内部也是会走查找set方法,有的话就会执行;我们给setName方法下个断点,可以看到调用如下:
      图片.png

    2. 我们直接用self.去设置看看是什么效果

    - (void)testSetProperty {
        self.Name = @"JayHe";
        NSLog(@"Name = %@ name = %@", self.Name, self.name);
        // 输出结果:RuntimeLearning[24955:405445] Name = (null) name = JayHe
    }
    
    
    • 发现输出结果也不是我们想要的,明明是设置self.Name为什么是设置给了name了

    3.我们修改两个属性的定义顺序看看效果

    @interface TestMethodInvocation ()
    
    @property (nonatomic, copy) NSString *Name;
    @property (nonatomic, copy) NSString *name;
    
    @end
    
    @implementation TestMethodInvocation
    
    - (void)testSetPropertyWhenChangeDeclareOrder {
        self.Name = @"JayHe";
        NSLog(@"Name = %@ name = %@", self.Name, self.name); // RuntimeLearning[25015:409263] Name = JayHe name = (null)
    }
    
    @end
    
    • 发现Name定义在前,这个时候能正确的设置值给Name了。

    到这里,我们发现谁定义在前面,那么调用set方法设置值的时候,就设置给了谁; 这是由于name、Name的set方法都是setName,谁定义在前面最终调用的就是设置谁的set方法了。

    猜测:这里其实就应该是方法覆盖的问题了,先申明的属性的setName覆盖了后申明的属性的setName方法;有点类似类别中定义了跟类一样的方法,会覆盖原类中的方法。

    验证猜测

    我们打印方法列表出来看看

    打印方法列表有2种方式
    写代码的方式:

    // 需要导入头文件#import <objc/runtime.h>
    - (void)logMethodListDescription {
        unsigned int count;
        Method *mList = class_copyMethodList(self.class, &count);
        for (unsigned int i = 0; i < count; i++) {
            Method method = mList[i];
            struct objc_method_description *description = method_getDescription(method);
            NSLog(@"%@", NSStringFromSelector(description->name));
        }
        free(mList);
    }
    

    输出如下

    2020-03-14 15:08:31.689850+0800 RuntimeLearning[25110:425278] logMethodListDescription
    2020-03-14 15:08:31.690129+0800 RuntimeLearning[25110:425278] testKVCSetProperty
    2020-03-14 15:08:31.690294+0800 RuntimeLearning[25110:425278] testSetProperty
    2020-03-14 15:08:31.690430+0800 RuntimeLearning[25110:425278] testSetPropertyWhenChangeDeclareOrder
    2020-03-14 15:08:31.690547+0800 RuntimeLearning[25110:425278] .cxx_destruct
    2020-03-14 15:08:31.690719+0800 RuntimeLearning[25110:425278] name
    2020-03-14 15:08:31.690838+0800 RuntimeLearning[25110:425278] setName:
    2020-03-14 15:08:31.690946+0800 RuntimeLearning[25110:425278] setName:
    2020-03-14 15:08:31.691072+0800 RuntimeLearning[25110:425278] Name
    

    lldb调试输出:

    (lldb) po [self _shortMethodDescription]
    <TestMethodInvocation: 0x600001ae5d00>:
    in TestMethodInvocation:
        Properties:
            @property (copy, nonatomic) NSString* Name;  (@synthesize Name = _Name;)
            @property (copy, nonatomic) NSString* name;  (@synthesize name = _name;)
        Instance Methods:
            - (void) logMethodListDescription; (0x10e996820)
            - (void) testKVCSetProperty; (0x10e9965e0)
            - (void) testSetProperty; (0x10e9966a0)
            - (void) testSetPropertyWhenChangeDeclareOrder; (0x10e996750)
            - (void) .cxx_destruct; (0x10e996990)
            - (id) name; (0x10e996960)
            - (void) setName:(id)arg1; (0x10e996920)
            - (void) setName:(id)arg1; (0x10e996920)
            - (id) Name; (0x10e9968f0)
    (NSObject ...)
    
    (lldb) 
    

    我们可以看到setName方法确实是有2个,那么调用的时候方法查找找到的是方法列表前面的那个,现象上看起来就是后面的setName方法被覆盖了。

    类别中添加与类同名的方法的情况

    • 添加一个类别,也实现setName方法
    @implementation TestMethodInvocation(Category)
    
    - (void)setName:(NSString *)Name {
        NSLog(@"类别中的方法调用了");
    }
    
    @end
    
    • 我们发现调用的是类别中的方法


      图片.png
    • 打印方法列表看看
    (lldb) po [self _shortMethodDescription]
    <TestMethodInvocation: 0x6000035d5300>:
    in TestMethodInvocation:
        Properties:
            @property (copy, nonatomic) NSString* Name;  (@synthesize Name = _Name;)
            @property (copy, nonatomic) NSString* name;  (@synthesize name = _name;)
        Instance Methods:
            - (void) testKVCSetProperty; (0x10bce4590)
            - (void) testSetProperty; (0x10bce4650)
            - (void) testSetPropertyWhenChangeDeclareOrder; (0x10bce4700)
            - (void) logMethodListDescription; (0x10bce47b0)
            - (void) .cxx_destruct; (0x10bce4920)
            - (id) name; (0x10bce48f0)
            - (void) setName:(id)arg1; (0x10bce4970)
            - (void) setName:(id)arg1; (0x10bce48b0)
            - (void) setName:(id)arg1; (0x10bce48b0)
            - (id) Name; (0x10bce4880)
    (NSObject ...)
    
    (lldb) 
    

    这个时候方法列表中有三个setName方法了;并且调用的时候调用的是分类的方法,那么可以知道分类的方法是在方法列表的前面了,现象上就是分类的方法覆盖了原类的方法了

    • 面试题:分类实现了跟原类同样的方法,问怎么调用原类的方法。

    我们知道通过方法调用查找,找到的是方法列表前面的方法了(分类中实现的方法),那么如果想调用原类的方法,那就遍历方法列表,拿到原类的实现去调用就好了。

    示范代码如下:
    我们可以根据需要去找到想要调用的sel的imp调用就完事了

    - (void)callOriginalClassMethod {
        unsigned int count;
        Method *mList = class_copyMethodList(self.class, &count);
        unsigned int findIndex = 0;
        for (unsigned int i = 0; i < count; i++) {
            Method method = mList[i];
            struct objc_method_description *description = method_getDescription(method);
            NSLog(@"%@", NSStringFromSelector(description->name));
            if (description->name == @selector(setName:)) {
                findIndex = i;
            }
        }
        if (findIndex) {
            Method method = mList[findIndex];
            SEL selector = method_getName(method);
            IMP imp = method_getImplementation(method);
            ((void (*)(id, SEL, NSString *))imp)(self,selector, @"JayHe");//_objc_msgForward
            //            ((void (*)(id,SEL, NSString *))objc_msgSend)(self, selector, @"JayHe");
        }
        
        free(mList);
    }
    

    总结:

    • 其实这种同属性名,就是首字母大小写不一样的方式定义2个属性不符合代码规范的;一般可能出现在面试题中考验你对方法列表、方法调用的理解
    • 上述示例中的属性,如果想正确赋值,有2中方式:
    1. 直接使用实例变量赋值不要使用set方法
    2. 我们可以通过遍历方法列表拿到imp去调用我们想要调用的方法
    • 分类的方法列表是运行时在类初始化的时候,加到原类的方法列表头部的,这个可以在runtime的源码的methodizeClass(cls)方法查看逻辑;这也是为什么分类中定义了同名方法之后行为上覆盖了原类的方法的原因

    相关文章

      网友评论

          本文标题:iOS方法调用覆盖分析

          本文链接:https://www.haomeiwen.com/subject/kciwshtx.html