美文网首页iOS底层
类(一)-- 底层探索

类(一)-- 底层探索

作者: 过气的程序员DZ | 来源:发表于2020-09-12 22:23 被阅读0次

    类(一)-- 底层探索
    类(二)-- method归属
    类(三)-- cache_t分析

    这篇文章来探索类的底层


    类探索

    一个自定义类的类名是我们决定的,所以我们想进行类探索,就得看看我们定义的类,在底层是如何被定义的。

    1、将OC代码转换成c++代码

    先准备一个自定义类DZPerson,在类中定义一个成员变量(_nick),一个属性(nameStr)。实现了一个方法(-saySomethine)和一个类方法(+sayHello)

    @interface DZPerson : NSObject
    {
        NSString *_nick;
    }
    @property (copy, nonatomic) NSString *nameStr;
    @end
    
    @implementation DZPerson
    - (void)saySomething {
        NSLog(@"%s", __func__);
    }
    
    + (void)sayHello {
        NSLog(@"%s", __func__);
    }
    @end
    

    在main中进行初始化。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            NSLog(@"Hello, World!");
            
            DZPerson *person = [DZPerson alloc];
        }
        return 0;
    }
    

    使用终端,进入到main.m文件所在的路径,使用clang命令,编译main.m文件

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

    输出main.cpp,打开这个文件,在里面进行搜索DZPerson,作为线索的出发点:

    typedef struct objc_object DZPerson;
    
    ⏬⏬⏬
    
    struct DZPerson_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        NSString *_nick;
    };
    
    ⏬⏬⏬
    
    struct NSObject_IMPL {
        Class isa;
    };
    
    ⏬⏬⏬
    
    typedef struct objc_class *Class;
    
    • 自定义类的类名是struct objc_object结构体的别名。
    • 找到了一个struct DZPerson_IMPL结构体,包含两个成员属性:
      • 另一个结构体struct NSObject_IMPL,通过名字中的字段,了解到这个应该是父类NSObject
      • 我们定义的成员_nick
    • 查看NSObject_IMPL结构体,里面有一个成员isa,是Class类型。
    • 最后在文件中找到Classstruct objc_class *结构体指针类型。

    通过以上逻辑,我们的目标转移到struct objc_objectstruct objc_class这两个结构体上了。

    2、struct objc_object & struct objc_class

    看看这两个结构体在源码中的定义,代码比较长,我只截获了需要我们研究的部分:

    struct objc_object {
    private:
        isa_t isa;
    
    public:
        //这里定义了一些函数,先省略
        ...
    }
    
    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() const {
            return bits.data();
        }
        void setData(class_rw_t *newData) {
            bits.setData(newData);
        }
        //省略后续代码
        ...
    }
    
    • objc_object中只有一个成员:isa
    • objc_class继承自objc_object
      • 第一个成员就是父类objc_object中的isa(苹果的注释还是挺人性的)
      • superclass,字面意思很清楚,super类
      • cache,缓存相关信息
      • bits,里面存放类的相关数据(成员、属性、方法、协议等相关数据信息)
      • 后面还有很多函数,可以自行去查看一下。(此处记录bits的getter和setter函数,因为后面要用到)

    通过以上的源码查看和相关的简要分析,我们可以确认自定义类在底层会被转化成objc_class类型的结构体。实例对象在底层就是objc_object类型。通过下面的两行源码,可以直接证明:

    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    

    此时看文章的你可能会有个疑惑:objc_class继承自objc_object,==那么类也是对象?==

    3、万物皆对象

    OC中类也是对象,如何证明呢?我们知道,实例对象中的isa指向的是类,类中也有isa,我们通过lldb来看看类的isa:
    我们以文章开头的代码为例:

    DZPerson *person = [DZPerson alloc];
    

    lldb执行截图:


    1. 打印person的内存情况x/4gx perosn:首地址中的第一个值是person对象的isa。
    2. 从person的isa中读取类信息地址(通过一个系统定义好的宏,进行获取)。
    3. po得到的值,也就是图片中第一个红框,打印是DZPerosn,地址是:0x0000000100002378
    4. 再用与查看person内存相同的方式,查看得到的0x0000000100002378的内存情况,也就是查看DZPerson类的内存
    5. 获取DZPersonisa,并po一下,打印的结果页是DZPerson,但是地址和前面打印的地址不同。这个第二个DZPerosn元类
    6. 继续查看元类isa,打印出来的是NSObject,但是它不是NSObject类,而是NSObject的元类,也就是根元类。而根元类的isa指向还是根元类

    简单来说,==实例对象isa指向类,类isa指向元类,元类isa指向根元类,根元类的isa指向根元类。==通过一张图,可以更好的理解isa的走位指向:


    isa流程图

    图中虚线代表isa走位,实现代表superclass走位。

    ==因此:类是元类的实例对象,而元类是根元类的示例对象。所以说,类也是对象,万物接对象==。

    4、lldb调试打印类中的信息

    接下来我们用lldb来看看类中存储的属性、方法,前文说到这些信息都存储在class_data_bits_t bits中,使用指针偏移的方式就可以获取到bits。
    此处放一张objc_class的源码截图:

    拿到class在内存中的首地址,分别加上属性isa、superclass和cache占用的内存大小,就可以得到bits的地址。

    isasuperclass都是指针类型,所以分别占用8字节。

    那么现在需要计算cache占用多少字节,看看cache_t的源码:

    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;
        
        //省略静态成员变量
        ...
    #else
    #error Unknown cache mask storage type.
    #endif
        
    #if __LP64__
        uint16_t _flags;
    #endif
        uint16_t _occupied;
    
    public:
        //省略后面的函数
        ...
    }
    
    • 首先是编译条件的公式#if和#elif,里面都是有两个成员变量,分别是指针类型和mask_t类型。指针占用8个字节,mask_tuint32_t的别名,也就是占中4个字节。
    • _flags_occupied分别占用2个字节,因为是uint16_t类型。
    • 其余的就是静态成员和函数,在结构体中的静态成员和函数是不占用结构体内存空间的。因此得出cache_t cache成员在结构体中占用16字节

    ==那么想要得到class_data_bits_t bits的偏移量,就是isa+superclass+cache=8+8+16=32,转换成十六进制就是20==

    条件满足,我们开始打印类中的信息:

    • 打印类地址:直接获取DZPerson的首地址
      首地址的第一个元素就是类的isa,所以我们直接以十六进制打印即可。
    • 获取bits,拿到首地址进行+20偏移,20是前面计算出来的(0x0000000100002398 = 0x0000000100002378 + 20)。并且进行强制类型转换
    • 调用class_rw_t *data() const函数(前文中,objc_class源码中的函数),得到地址,打印地址中的值:
    • 接下来用class_rw_t中的methods()函数,可以获取到方法列表:
    • list函数获取列表的首地址,打印地址的值后,用get查看到类中的所有方法。通过下图能看到类中定义的saySomething方法、属性nameStr的setter和getter方法,还有一个系统生成的.cxx_destruct方法:

    • 类中还定义了一个类方法+ (void)sayHello,在这里是看不到的。因为类方法存在元类中。

    通过上面的方式我们就可以查看到类中方法,也可以证明,方法是存在类中。同样,通过class_rw_t中的properties()可以获取到属性、protocols()获取协议。

    成员变量存在ro中,通过函数ro()获取:


    总结

    1. 通过查看c++代码,我们找到了类的底层定义objc_class,并且知道类中也存在isa指针。
    2. 通过isa走位图,知道类也是对象,并推到出‘万物皆对象’。
    3. 通过lldb一步步看到类中的属性、方法、协议以及成员在类中如何存储的。

    希望本文能对你有所帮助,如果有错误的地方,还请指出,谢谢!

    相关文章

      网友评论

        本文标题:类(一)-- 底层探索

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