美文网首页
第五节—类的结构分析

第五节—类的结构分析

作者: L_Ares | 来源:发表于2020-10-22 20:16 被阅读0次

    本文为L_Ares个人写作,包括图片皆为个人亲自操作,以任何形式转载请表明原文出处。

    上一节,我们通过Clang的方式得到了可以查看的编译文件,从而看到了Objc的类的本质——结构体。

    本节则将通过Clanglldb命令,探索一下,类这个结构体的结构。查看一下类的结构中的元素的意义,以及我们经常使用的成员变量、实例变量、属性、实例方法、类方法等等都在类的哪些结构中。把类的骨架探索出来。

    一、类和元类的生成时期

    有关于对象的生成时期,大家应该很清楚,对象都是在我们书写代码的时候由类申请的内存,从而生成了对象。

    那么,类和元类是什么时候生成的呢?

    答案 : 在编译期。

    怎么验证?

    还是用之前的Objc781可编译源码来尝试。

    大家都知道,当我们在使用xcode的时候commond + B可以编译但不执行我们的程序,并且可以在Products文件夹里面找到编译完成的文件。

    那么我们可以通过machOView这个软件来查看一下,commond + B完成编译后,我们定义的类,还有系统中的元类,有没有出现。

    machOView提取码 : spij

    步骤 :

    main.m文件中,我们创建一个JDPerosn类。然后commond + B

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    @interface JDPerson : NSObject
    
    @end
    
    @implementation JDPerson
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            
            //0x00007ffffffffff8
            JDPerson *person = [JDPerson alloc];
            
            NSLog(@"Hello, World!");
            
        }
        return 0;
    }
    

    编译结果 :

    很明显,没有stop running的选项,证明我没有执行,有提示显示,证明我编译过了。

    图1.png

    操作 :

    看到那个Products文件夹中的JDTest了吗,那就是编译后的文件,直接拖入machOView

    图2.png

    最终结果 :

    直接找到类关联,看画红框的里面——JDPerson。类出来了吧,那么元类呢?还记得isa的指向吗?类也有isa吧,指向的是元类吧,那证明了什么?证明了类是元类生出来的吧,类都出来了,元类能没有吗?

    证明结束。

    结论就是 :

    类和元类是在编译期就生成的。

    二、类是哪个结构体

    即然要探索类的结构,那么我们就获取一个对象的类在系统环境下,它的结构是什么样子的?

    所以先创建一个类对象,然后获取它的类。

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    @interface JDPerson : NSObject
    
    @end
    
    @implementation JDPerson
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            
            //0x00007ffffffffff8
            JDPerson *person = [JDPerson alloc];
            Class pClass = object_getClass(person);
            NSLog(@"Hello, World!");
            
        }
        return 0;
    }
    

    通过rumtime的object_getClass,我们可以获取一个对象的类在系统编译情况下的一个类的信息。并且返回一个Class的对象。但是发现Class对象不能点进去看,于是我把这个main.m文件的编译文件打开看看,这个Class到底是什么。

    操作 :

    进入到当前项目的main.m所在的文件夹下,执行以下命令。

    clang -rewrite-objc main.m -o main.cpp
    

    这时候main.m的文件夹下会出现经过clang编译以后的main.cpp文件。

    结果 :

    图3.png

    操作 :

    打开main.cpp,找到我们当前的这个JDPerson

    结果 :

    图4.png

    操作 :

    原来是重命名的结构体,那objc_object又是什么呢?继续搜索它。搜索struct objc_object,看看有没有和Class相关的。

    于是找到了

    结果 :

    图5.png

    并且发现了一个结构,就是objc_object这个结构体里面,唯一的元素是Class,正是object_getClass返回类型,而且发现了Class也是一个重命名的结构体——objc_class

    操作 :

    即然找到了Class的本质,那么就回到项目里面,直接在project下搜索源码查看一下,这个struct objc_class的结构。

    结果 :

    图6.png

    结论 :

    原来object_class这个结构体是继承与objc_object的。所以类的本质都是一个objc_object结构体。

    问题 :

    • 为什么找Class
    • 为什么不是runtime.h中的objc_class
    • 为什么不进objc-runtime-old.hobjc_class

    操作 :

    图7.png
    • 因为通过object_getClass,我们万物的类都可以用Class接收。

    • runtime.h中的objc_class后面明显写了弃用。

    • 不进old是因为有新的。看新不看旧。

    这里就可以放另一个神图了,相信大家也都见过。从上面的分析一起看,就更清楚这个神图的走势了。

    图8.png

    三、类的结构

    1. 最普通的类的结构探索

    看图6,解释一波这些结构体里面的元素都是什么

    • Class ISA : isa指针。为什么被注释了?因为它是客观存在的,但又不是objc_class自己的,那它是谁的呢?明显来自于它的父类objc_object

    • Class superclass : objc_class的父类。

    • cache_t cache : 以前缓存的指针和虚函数表

    • class_data_bits_t bits : class_rw_t *加上自定义rr/alloc标志。

    我们结合lldb看一下这个类的实例的内存情况 :

    图9.png

    前面两个还可以看出来一个是isa的指针指向,一个是superClass,第三个开始就找不到了。

    那么后面的两个都是什么呢?下面的开始探索。

    2. 类的属性存储

    上面已经知道了一个普通的没有属性的类的结构,那么如果我们加上属性,属性会存储到哪里呢?

    直接在JDPerson里面定义一个myJDName属性和一个JDHobby成员变量。

    @interface JDPerson : NSObject
    {
        NSString *JDHobby;
    }
    
    @property (nonatomic,copy) NSString *myJDName;
    
    @end
    

    我们来找一下,这两个在类中定义的变量都存储在类的哪个结构里面。

    先来一波猜测,因为上面我们已经看到了类的结构了。

    第一个,isa是不可能的,po地址都告诉我们了,人家指针里面存的是指向的地址,指向的是元类。

    第二个,superClass。也po出来了,是NSObject

    第三个cache_t开始po不出来了,但是第三个地址也说明了,那是缓存。

    所以,应该就是在第四个变量中class_data_bits_t bits字节中。

    但是第三个开始都读不出来了呀,而且图9都看到了,第四个地址是0啊,根本po不出东西呀。

    那么我们改变方法,即然直接po不出东西,我们就计算指针,直接po第四个的内存地址。

    方法 :

    • 已知条件:
    1. Class对象的首地址 :
    图10.png
    1. isa指针的大小 : 都说了是指针,所以,8字节

    2. superclass的大小,这是一个Class的变量吧,进去Class看,这是结构体指针呀,typedef struct objc_class *Class;也是8字节

    其实直接根据0x1000081c00x1000081d0就可以了,前两个都说了是isasuperClass,那第三个就是cache_t呗。

    思路 :

    cache_t cache的首地址 + cache_t cache的内存大小 = class_data_bits_t bits的首地址

    拿到首地址,我们就拿到了它的对象,再从源码中找到class_data_bits_t的结构,根据这个结构,去找到属性在哪里,是不是就ok了?

    所以思路上的拦路虎 : cache_t cache是多大?

    2.1 搞清楚cache_t cache多大

    点进去看cache_t的结构。

    struct cache_t
    

    多余的我就不贴了,反正是个结构体,结构体的大小等于结构体里面的元素的大小的总和,所以我要找属于这个结构体自己的内部元素。

    所以,刨除所有的static,因为static静态变量吧,人家在自己的全局区里面。

    剩下的如下 :

    
    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets;
        explicit_atomic<mask_t> _mask;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
    #endif
    
    #if __LP64__
        uint16_t _flags;
    #endif
        uint16_t _occupied;
    }
    
    

    public里面不要管了吧,那是人家的函数。

    先说这句宏#if __LP64__,64位系统都会有,所以这个宏就当没有就好了。

    然后就有一个很友好的处理,它的宏判断if里面的大小是一模一样的。

    if里面 :

    • <struct bucket_t *> _buckets : 结构体指针,8字节

    • <mask_t> _mask ,进mask_t看一下是typedef uint32_t mask_t; : 就是无符号整型4字节

    两个elif里面 :

    • uintptr_t : 看到ptr这三个字母就知道是指针了,所以,8字节

    • mask_t跟上面一样,4字节

    再看,下面两个不需要判断的 :

    • uint16_t : 这是unsigned short,所以,2字节。

    结论 :

    cache_t cache的内存大小 = 8 + 4 + 2 + 2 = 16

    2.2 获取class_data_bits_t bits的数据

    直接计算吧,16进制加减法,上面都说清了。用0x1000081c0的话,就加32,用0x1000081d0的话,就加16。结果是 : 0x1000081e0

    所以,直接去读这个地址上的内存。

    操作 :

    lldb输入 : p (class_data_bits_t *)0x1000081e0

    结果 :

    图11.png

    为什么要加(class_data_bits_t *),因为我们通过逻辑上的分析,已经确认了这里就是class_data_bits_t类型的指针。

    这时候,我们已经拿到了objc_class里面的bits的指针,并且是class_data_bits_t这个类型的指针。

    然后我们进入class_data_bits_t,看看怎么能拿到它的数据。

    图12.png

    函数中的第一个就是data()函数,而且知道了返回的data()类型是class_rw_t

    操作 :

    lldb输入 : p $1->data()

    结果 :

    图13.png

    我们查看bits.data()里面有什么东西。

    操作 :

    lldb输入 : p *$2

    结果 :

    图14.png
    2.3 类的属性所在

    通过2.2的查看bits.data()我们知道了类的数据信息都存放在了class_rw_t里面,但是属性在哪里,还没找到。

    可是看图14,我们可以知道的是,这些属性里面,只有Value可能存在着属性,因为其他的元素都用结果表明了它们不可能存储着类的成员变量和属性。

    这个时候其实已经很难探索了,因为找不到下一步的思路,只能站在上帝视角先把已知的知识拿来用,继而验证它的正确性,得到我们的结果。

    这里其实有一步试错,因为在class_rw_t里面我找到了property_array_t类型的数组,在里面找到了list()函数,但是拿到的只有定义的成员变量JDHobby,没有属性以及属性产生的myJDName的成员变量。

    我们知道的是类的属性存储在roivars里面,那就验证以下,是不是这样的。

    操作 :

    图15.png

    拿到class_rw_t变量的ro地址。

    打印ro中的数据信息 :

    图16.png

    获取ivar的内容 :

    图17.png

    这里,我们发现,我们取ivar_list_t的首地址的内容,在这个ivar_list_t里面,有我们的成员变量JDHobby,并且ivar_list_t应该是一片连续的内存空间,而且count = 2,说明有两个元素,所以直接get(0)get(1)看一下。

    图18.png

    这时候,我们找到了我们的一个成员变量,一个属性,存储在ivar_t类型的数据结构里面。那么这个ivar_t又是什么呢?

    图19.png

    这也就是我们常说的Ivars

    到这里,我们总结一下,类的属性在类中的实际结构 :

    图20.png

    结论 :

    • 类中的成员变量和属性都会存储在class_ro_t中的ivar_list_tivars地址上,作为ivar_list_t的元素,成员变量和属性的类型都统一为ivar_t

    问 :

    成员变量不是在class_rw_t中的property_array_t中可以找到吗?为什么不是存储在class_rw_tproperty_array_t中?

    答 :

    这个后面会说,大体的原因是因为class_rw_t在编译前期是没有的,只有class_ro_tclass_rw_t是在运行时态的加持下,在运行时才被copy了class_ro_t出来的。

    2. 类的方法存储

    2.1 类中只有属性没有方法时

    经历了类的属性存储的探索,类的方法的探索就变得很容易了。

    先看一下,最开始我们没有添加任何的方法的时候,class_ro_t里面出现了baseMethodList这样的一个元素,那看一下,它是什么。

    图21.png

    操作 :

    图22.png

    结果出来了3个方法???但是我明明没有定义呀,全部打印出来看。

    图23.png

    这就很理解了,全都是属性myJDName的方法,settergetter还有一个c++的析构函数。

    2.2 当类中自定义了方法时
    @interface JDPerson : NSObject
    {
        NSString *JDHobby;
    }
    
    @property (nonatomic,copy) NSString *myJDName;
    
    - (void)studyWork;
    
    + (void)eatFood;
    
    @end
    
    @implementation JDPerson
    
    - (void)studyWork {
        
    }
    
    + (void)eatFood {
        
    }
    
    @end
    

    现在定义一个实例方法,一个类方法。

    我们继续探索属性的时候的思路,这次我就直接贴图片了。需要说明的会下面说明。

    图24.png

    只有4个方法在class_ro_tmethod_list_t * baseMethodList里面。

    图25.png

    唯独缺少类方法。

    截止到这里,我们可以确定

    类的实例方法存储在class_ro_tmethod_list_t * baseMethodList里面。

    2.3类方法所在

    通过前几节的总结,应该可以得出这样的一个猜测 :

    根据对象的实例方法存储在类中,那么类的类方法是不是元类的实例方法?万物皆对象啊,所以我们可以去找元类看看,元类里面有没有类方法。

    1. JDPerson的类地址

      图26.png
    2. 利用ISA_MASK计算JDPerson元类的首地址

    图27.png
    1. 把指针从首地址移动32字节,变成0x00000001000081e8,移动到JDPerson元类的class_data_bits_t bits上面。
    图28.png
    1. 调用objc_classdata函数,获取数据的指针,并读取指针指向的内容。
    图29.png
    1. 调用class_rw_tro()函数,拿到类的属性和方法编译后的数据存储地址指针,class_ro_t指针,并打印指针指向的内容。
    图30.png
    1. 拿到JDPerson元类的method_list_t *baseMethodList
    图31.png

    这里就发现了类方法eatFood

    结论 :

    类方法eatFood放在了JDPerson的元类里面。

    四、类的结构的总结

    • Objc中的类,本质上都是结构体objc_objectClass类的本质也是objc_object,只不过是它的子类objc_class

    • Objc中的成员变量和属性全部都存储在当前类的class_ro_t中的ivar_list_t类型变量ivars里面。并且它们统一的类型都是ivar_t

    • Objc中的实例方法存储在当前类的class_ro_t中的method_list_t类型的变量baseMethodList里面。

    • Objc中的类方法存储在当前类的元类的class_ro_t中的method_list_t类型的变量baseMethodList里面。

    下面上几张图,来看一下类的结构

    图32.png 图33.png 图34.png

    相关文章

      网友评论

          本文标题:第五节—类的结构分析

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