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

OC底层原理 学习大纲

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

今天我深入拓展一下:

    1. 怎么知道成员变量、属性存放位置的?
    1. v24@0:8@16这种符号啥意思?(熟悉编码表)
    1. 类的归属

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

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

  • 可以给一个特殊的名称
  • 通过clang将文件静态编译成cpp文件
  • 搜索特殊名称。查看系统存放位置。

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

  • 定义一个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

  • SEL: 方法编号
  • IMP: 函数指针地址
  • SEL和IMP: 组成键值对

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

  • 打开Xcode开发者文档
    开发者文档.png

搜索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

class_copyMethodList

大致意思是说:

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

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

image.png

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

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

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

  • HTPerson只有sayHello一个实例方法。所以class_copyMethodList打印的只有是sayHello

  • image.png
- 问题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);
    
}
  • 这个需要弄清楚class_getInstanceMethod的原理

官方解释:

image.png

大致意思是说:

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

源码分析:

image.png

进入lookUpImpOrForward。 寻找imp

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

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

  • lookUpImpOrForward,就是使用一切办法(本类、父类、消息转发等)将Imp找到。
  • class_getInstanceMethod, 因为上面lookUpImpOrForward已经将imp找到了。所以class_getInstanceMethod就只需要走类继承(superclass)这条线来常规读取imp了。先在自己本类找,找不到再到superclass中寻找。如果运行到根类, 还找不到就条件终止(根类父类nil),返回nil

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

  • 这里sayHello是实例方法,sayHappy是类方法。 所以打印结果中,第一个第四个有值,第二个第三个

  • 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);
}
  • 这个需要弄清楚class_getClassMethod的原理

官方解释:

image.png

大致意思是说:

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

源码分析:

image.png

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

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

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

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

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

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

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

  • 第三个呢? 第三个上面说了,class_getClassMethod只处理类方法,如果不是元类,就帮你转成元类
  • 所以第三个实际上是转成了元类,再调用元类中储存的类方法

所以打印结果如下:

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);
}
  • 这个需要弄清楚class_getMethodImplementation的原理

官方解释:

image.png

大致意思是说:

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

源码分析:

image.png

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

我们知道:

  • 第一个实例方法第四个类方法,本身是可找到对应的imp的,所以是存在的。

  • 第二个第三个,我们会调用消息转发机制。

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


image.png

总结

  • class_getInstanceMethod: 获取实例方法
    自身类->父类->...->根类->nil这一条线,只要能找到对应实例方法,就返回方法imp;否则,返回null

  • class_getClassMethod: 获取类方法
    如果传入的不是元类,就转成元类再调用class_getInstanceMethod方法。 总之,类方法只存在元类中

  • class_getMethodImplementation: 获取方法的实现
    如果未找到,就进行消息转发

面试题二: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继承图:

  • isa指向和superclass继承

掌握秘诀,答题开始:

  • re1:
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

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

答案: False

  • re3:
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

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

答案: False

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

答案: True

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

答案: True

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

答案: True

  • re8:
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还是直接比较

开始答题:

  • re9:
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

  • re10:
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

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

答案: False

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

答案: False

附上打印结果

答案

恭喜你,挑战成功!

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271