iOS 底层原理 iOS 进阶之路

OC底层原理十: 类的深入理解

2020-09-15  本文已影响0人  markhetao

OC底层原理 学习大纲

上一节我们分析了类的结构成员变量属性实例方法类方法的读取。

今天我深入拓展一下:

1. 怎么知道成员变量、属性存放位置的?

当我们只知道objc_class结构的时候,无法确定某类属性或方法存放在哪里时:

例如: 我们不知道成员变量存放在哪。

  • 定义一个hobby成员变量
@interface HTPerson : NSObject {
   NSString * hobby;
}
  • main.m中实例化HTPerson对象

  • clang -rewrite-objc main.m -o main.cpp 编译成cpp静态文件。

  • 打开main.cpp文件,搜索hobby

    image.png
    image.png
    image.png
  • 因为是特殊名称(系统中基本不会出现的名称),所以一下就可以搜索出来。

  • 上面就是这个名称出现的所有位置。 我们对比objc_classdata()数据格式来看:

    image.png

发现class_ro_t类型的只有ro()函数。

  • 进入class_ro_t查看格式。
    image.png

顺利找到ivar_list_t。定位了成员变量位置。
实践过程可查看上一节

这里介绍的是通用定位方法。 不局限于成员变量

2. v24@0:8@16这种符号啥意思?

我们查看cpp文件时,发现函数都有v24@0:8@16这样的符号。

image.png
SEL 和 IMP

每个方法都有SELIMP

我们调用函数,是调用OC上层封装好的函数。 通过SEL方法编号 -> 找到对应的IMP指针地址 -> 返回指针指向底层实现

搜索ivar_getTypeEncoding

image.png

点击进入官方文档,查看所有类型编码 👉 快捷通道

image.png

这些就是各类型简写。 以后看到这些就清晰明朗了。😃

v24@0:8@16 :
v: void , @: 一个对象 ,:SEL 函数选择器

描述:定义一个返回值是void,总内存大小为24字节的函数,第一个入参是对象,从0字节开始,第二个入参SEL,从8字节开始,第三个入参是对象,从16字节开始

对比下面实际函数看就一目了然了:

  • static void I_HTPerson_setName(HTPerson *self, SEL _cmd, NSString *name)
    第一个入参HTPerson实例对象, 第二个入参SEL方法编号, 第三个入参name字符串

类似属性值也可以参看相关文档了解
{"name","T@\"NSString\",&,N,V_name"
文档中搜索property_getAttributes, 找到👉 官方描述

3. 类的归属

上一节在结束时,我们验证了

面试题一: 类的归属

准备测试数据

@interface HTPerson : NSObject
- (void)sayHello;
+ (void)sayHappy;
@end

@implementation HTPerson

- (void)sayHello{
    NSLog(@"HTPerson say : Hello!!!");
}

+ (void)sayHappy{
    NSLog(@"HTPerson say : Happy!!!");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        HTPerson *person = [HTPerson alloc];
        Class pClass     = object_getClass(person);

        // 这里加测试函数
        // Objc_copyMethodList(pClass);                 // 问题1
        // InstanceMethod_classToMetaclass(pClass);     // 问题2
        // ClassMethod_classToMetaclass(pClass);        // 问题3
        // IMP_classToMetaclass(pClass);                // 问题4
    }
    return 0;
}
- 问题1:Objc_copyMethodList打印的函数有哪些?
void Objc_copyMethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        
        NSLog(@"Method, name: %@", key);
    }
    free(methods);
}

月月这位优秀童鞋那学来的学习方法,分享下:

  • 不懂的先查官方文档,清楚功能。再去源码究其根源。

官方解释:
打开帮助文档:


官方文档

搜索 class_copyMethodList

class_copyMethodList

大致意思是说:

  • 如果这个类有实现了的实例函数,就返回一个包含所有实例函数的数组。最后你必须free释放这个数组。
  • 如果当前类没有实例函数,或者当前类为,返回Null.并且outCount为0

进入objc4源码中,进入class_copyMethodList方法。

image.png

我们可以看到完整的流程。 从HTPerson类中读取data()methods()函数。并将其另辟空间result存储后,返回result

  • 这个result就是类的Methods函数数组。学完上一节我们知道,类的Methods中存储的是所有实例方法类方法是存储在元类中的。

  • 看完源码,你应该要知道代码最后为何要加上free了吧。 因为result是新开辟的空间,需要手动释放。

- 问题2:InstanceMethod_classToMetaclass中哪些函数不是0x0?
void InstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%s:", __func__);
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    
}

官方解释:

image.png

大致意思是说:

  • 如果传入的或它的父类不包含这个实例方法,就返回Null

源码分析:

image.png

进入lookUpImpOrForward。 寻找imp

image.png
  • 进入_class_getMethod
_class_getMethod
  • 进入getMethod_nolock
image.png

通过熟悉源码和文档,我们可以知道:

通过上一节学习,我们知道,实例方法存在中,而类方法是存储在元类中的。

- 问题3:ClassMethod_classToMetaclass中哪些函数不是0x0?
void ClassMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));

    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%s", __func__);
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
}

官方解释:

image.png

大致意思是说:

  • 如果传入的或它的父类不包含这个实例方法,就返回Null

源码分析:

image.png

发现这个函数跟问题2进入的一样,都是class_getInstanceMethod

  • 不同的是这里入参是getMeta, 我们进入查看:
image.png

发现个有意思的事情。 我们传入cls

  • 代码判断isMetaClass是否为元类,如果是元类就返回cls本类,如果不是, 我就返回本类的isa

用大白话解释:
兄弟,类方法都放在元类里。如果你给我的是元类,我可以直接操作。 如果不是元类,我就免费帮你转成元类(类的isa指向元类),再去操作。总之,我这只办理元类的业务。

  • CC++的层面不存在类方法对象方法,一切方法的处理,在它确定对象(类还是元类)之后,都交给class_getInstanceMethod来处理。 所以后续方法就跟问题2的处理一样了。

因为这题是获取类方法。 题中类方法只有sayHappy,所以第一个第二个都是不存在。 类方法是存在元类中,所以第四个存在的。

所以打印结果如下:

image.png
- 问题4:IMP_classToMetaclass中哪些函数不是0x0?
void class_getMethodImplementation(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);

    IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));

    IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy));
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));

    NSLog(@"%s:",__func__);
    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
}

官方解释:

image.png

大致意思是说:

  • 如果传入的或它的父类都不包含这个实例方法,就使用运行时的消息转发机制。

源码分析:

image.png

我们发现,如果我们找不到方法对应的imp,就使用运行时的消息转发。让能处理这个消息的对象接收处理。

我们知道:

我们打印查看,最终都是存在的。


image.png

总结

面试题二:isKindOfClass 与 isMemberOfClass

打印小技巧:

#ifdef DEBUG
#define HTLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## >__VA_ARGS__] UTF8String]);
#else
#define HTLog(format, ...);
#endif
@interface HTPerson : NSObject
@end

@implementation HTPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
        BOOL re3 = [(id)[HTPerson class] isKindOfClass:[HTPerson class]];
        BOOL re4 = [(id)[HTPerson class] isMemberOfClass:[HTPerson class]];
        HTLog(@" re1 :%hhd        re2 :%hhd        re3 :%hhd        re4 :%hhd",re1,re2,re3,re4);

        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
        BOOL re7 = [(id)[HTPerson alloc] isKindOfClass:[HTPerson class]];
        BOOL re8 = [(id)[HTPerson alloc] isMemberOfClass:[HTPerson class]];
        HTLog(@" re5 :%hhd        re6 :%hhd        re7 :%hhd        re8 :%hhd",re5,re6,re7,re8);
    }
    return 0;
}

官方文档

image.png image.png

源码分析

image.png

isKindOfClass

  • 类方法: 类的Isa指向的是元类,沿着父类(superclass)这条线寻找,检查是否存在和入参cls相等的
  • 实例方法: 对象的Isa指向的是本类,沿着父类(superclass)这条线寻找,检查是否存在和入参cls相等的

isMemberOfClass

  • 类方法: 判断Isa指向的元类,是否与入参cls相等。
  • 实例方法: 判断Isa指向的本类是否与入参cls相等。

💣 💥

当我们以为一切仅在掌控之中时,却发现isKindOfClass类方法实例方法压根没调用😭

加入断点

image.png

打开汇编模式

image.png image.png
  • 为什么要调用objc_opt_class呢?我们在objc源码层找不到调用依据,打开llvm搜索objc_opt_isKindOfClass
image.png

我们看到了熟悉的objc_alloc。你是否记得NSObject的alloc方法也没走objc4源码的alloc类方法? 而是llvm在编译层就将其处理好了。

  • 在这个表中,objc_opt_isKindOfClass静静地跟着objc_alloc一起躺着,我们看注释,可以知道苹果官方设计的原因,因为这些函数极少被改变,所以为了加速性能,苹果在llvm编译层就已经将其优化处理。 比如isKindOfClass如果没有被外部重写,在被调用时都是直接消息转发执行objc_opt_isKindOfClass

  • objc4源码中查看objc_opt_isKindOfClass内部实现:

image.png

发现内部实现就跟isKindOfClass的方法一样。

特点

  1. 类方法的初始值是元类实例方法的初始值是本类

  2. objc_opt_isKindOfClass: 是底层实现,有遍历操作

  • 类方法实例方法在底层统称为方法。(入参是实例对象,初始值为本类; 入参是,初始值是元类)
  1. isMemberOfClass: 类方法实例方法无遍历操作,直接比较

在开始之前,我们请上经典isa指向superclass继承图:

掌握秘诀,答题开始:

BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
  • isKindOfClass类方法,调用objc_opt_isKindOfClass,初始值为元类,拥有遍历操作
  1. 获取[NSObject class]isa指向的类: NSObject元类
  2. 判断NSObject元类NSObject类,不相等。
  3. 继续寻找NSObject元类superclass: NSObject类
  4. [NSObject class]相等。 返回true

答案: True

BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
  • isMemberOfClass类方法,初始值为元类,无遍历操作,直接比较
  1. 获取[NSObject class]isa指向的类: NSObject元类
  2. 判断NSObject元类NSObject类,不相等。 返回false

答案: False

BOOL re3 = [(id)[HTPerson class] isKindOfClass:[HTPerson class]];

-isKindOfClass类方法,调用objc_opt_isKindOfClass,初始值为元类,拥有遍历操作

  1. 获取[HTPerson class]isa指向的类: HTPerson元类
  2. 判断HTPerson元类HTPerson类,不相等。
  3. 继续层层寻找HTPerson元类superclass
  4. 依次找到: NSObject元类NSObject类nil。都与HTPerson类不相等,返回false

答案: False

BOOL re4 = [(id)[HTPerson class] isMemberOfClass:[HTPerson class]];
  • isMemberOfClass类方法,初始值为元类,无遍历操作,直接比较
  1. 获取[HTPerson class]isa指向的类: HTPerson元类
  2. 判断HTPerson元类HTPerson类,不相等。返回false

答案: False

BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
  • isKindOfClass对象方法,调用objc_opt_isKindOfClass,初始值为本类,拥有遍历操作
  1. 获取[NSObject alloc]的本类: NSObject类
  2. 判断NSObject类[NSObject class]相等, 返回True

答案: True

BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
  • isMemberOfClass对象方法,初始值为本类,无遍历操作,直接比较
  1. 获取[NSObject alloc]的本类: NSObject类
  2. 判断NSObject类[NSObject class]相等, 返回True

答案: True

BOOL re7 = [(id)[HTPerson alloc] isKindOfClass:[HTPerson class]];
  • isKindOfClass对象方法,调用objc_opt_isKindOfClass,初始值为本类,有遍历操作
  1. 获取[HTPerson alloc]的本类: HTPerson类
  2. 判断HTPerson类[HTPerson class]相等, 返回True

答案: True

BOOL re8 = [(id)[HTPerson alloc] isMemberOfClass:[HTPerson class]];
  • isMemberOfClass对象方法,初始值为本类,无遍历操作,直接比较
  1. 获取[HTPerson alloc]的本类: HTPerson类
  2. 判断HTPerson类[HTPerson class]相等, 返回True

答案: True

附上打印结果

答案

是不是掌握了诀窍之后,瞬间无对手了?


别飘? 咱们加设一题

BOOL re9 = [(id)[HTPerson class] isKindOfClass:[NSObject class]];
BOOL re10 = [(id)[HTPerson alloc] isKindOfClass:[NSObject class]];
BOOL re11 = [(id)[HTPerson class] isMemberOfClass:[NSObject class]];
BOOL re12 = [(id)[HTPerson alloc] isMemberOfClass:[NSObject class]];
HTLog(@" re9 :%hhd        re10 :%hhd        re11 :%hhd        re12 :%hhd",re9,re10,re11,re12);

记住我每个答案上第一行解题思路
什么函数对象方法还是类方法? 初始值是元类还是本类? 是遍历superclass还是直接比较

开始答题:

BOOL re9 = [(id)[HTPerson class] isKindOfClass:[NSObject class]];
  • isKindOfClass类方法,调用objc_opt_isKindOfClass,初始值为元类,拥有遍历操作
  1. 获取[HTPerson class]isa指向的类: HTPerson元类
  2. 判断HTPerson元类NSObject类,不相等。
  3. 继续层层寻找HTPerson元类superclass
  4. 依次找到: NSObject元类NSObject类
  5. NSObject类相等,返回True

答案: True

BOOL re10 = [(id)[HTPerson alloc] isKindOfClass:[NSObject class]];
  • isKindOfClass对象方法,调用objc_opt_isKindOfClass,初始值为本类,有遍历操作
  1. 获取[HTPerson alloc]的本类: HTPerson类
  2. 判断HTPerson类[NSObject class]不相等。
  3. 继续寻找HTPerson类superclassNSObject类
  4. 此时NSObject类[NSObject class]``相等,返回True

答案: True

BOOL re11 = [(id)[HTPerson class] isMemberOfClass:[NSObject class]];
  • isMemberOfClass类方法,初始值为元类,无遍历操作,直接比较
  1. 获取[HTPerson class]isa指向的类: HTPerson元类
  2. 判断HTPerson元类NSObject类,不相等。返回False

答案: False

BOOL re12 = [(id)[HTPerson alloc] isMemberOfClass:[NSObject class]];
  • isMemberOfClass实例方法,初始值为本类,无遍历操作,直接比较
  1. 获取[HTPerson alloc]的本类: HTPerson类
  2. 判断HTPerson类NSObject类,不相等。返回False

答案: False

附上打印结果

答案

恭喜你,挑战成功!

别吐槽,面试怎么虐得爽就怎么来 😂

我自己写的时候也很凌乱,直到整理出的解题思路,才打通任督二脉

什么函数对象方法还是类方法? 初始值是元类还是本类? 是遍历superclass还是直接比较

上一篇 下一篇

猜你喜欢

热点阅读