IOS底层原理之类结构分析

前言

阳光明媚,天气凉爽,不出去浪一浪,在这探究类,我堕落了啊。言归正传,前面探究对象时发现isa之间的关联。那么今天就来探究下,看看有什么神奇之处。

准备工作

内存偏移

前一篇博客 IOS 底层原理之对象的本质&isa关联类 探究了如果要想获取对象内存中的变量,底层实现方式是对象的首地址+偏移值。下面探究下内存偏移

普通指针

int main(int argc, char * argv[]) {
    @autoreleasepool {
       int a = 10;
       int b = 10;
       int * p = &a;
       int * q = &b;
       NSLog(@"---%d----%p---%p",a,p,&p);
       NSLog(@"---%d----%p---%p",b,q,&q);
    }
    return 0;
}
复制代码
2021-06-17 15:07:03.252239+0800 内存偏移[11954:347138] ---10----0x7ffedfe69c8c---0x7ffedfe69c80
2021-06-17 15:07:03.252592+0800 内存偏移[11954:347138] ---10----0x7ffedfe69c88---0x7ffedfe69c78
复制代码
  • ab的值都是10,但是ab的地址不一样,这就是常说的深拷贝
  • a的地址是0x7ffedfe69c8cb的地址是0x7ffedfe69c88,相差4字节,主要取决于a的类型
  • a>b>p>q的地址,且它们全部在栈区

图解如下

image.png

对象指针

int main(int argc, char * argv[]) {
    @autoreleasepool {
       LWPerson  * p1 = [LWPerson alloc];
       LWPerson  * p2 = [LWPerson alloc];
       NSLog(@"---%@----%p",p1,&p1);
       NSLog(@"---%@----%p",p2,&p2);
    }
    return 0;
}
复制代码
2021-06-17 16:00:37.697384+0800 内存偏移[12536:388334] ---<LWPerson: 0x600002000000>----0x7ffee357bc70
2021-06-17 16:00:37.697465+0800 内存偏移[12536:388334] ---<LWPerson: 0x600002000010>----0x7ffee357bc68
复制代码
  • alloc 开辟的内存在堆区局部变量开辟的内存在栈区
  • 堆区地址 –> 地址,栈区地址 –> 地址

图解如下

image.png

数组指针

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int c[4] = {1,2,3,4};
        int *d   = c;
        NSLog(@"%p - %p - %p ",&c,&c[0],&c[1]);
        NSLog(@"%p - %p - %p ",d,d+1,d+2);

        for (int i = 0; i<4; i++) {
            //(d+i) 取地址里面的值
            int value =  *(d+i); 
            NSLog(@"value = %d",value);
        }
    }
    return 
}
复制代码
2021-06-17 16:32:13.132035+0800 内存偏移[12919:415236] 0x7ffeebfd9c80 - 0x7ffeebfd9c80 - 0x7ffeebfd9c84
2021-06-17 16:32:13.132122+0800 内存偏移[12919:415236] 0x7ffeebfd9c80 - 0x7ffeebfd9c84 - 0x7ffeebfd9c88
2021-06-17 16:32:13.132206+0800 内存偏移[12919:415236] value = 1
2021-06-17 16:32:13.132287+0800 内存偏移[12919:415236] value = 2
2021-06-17 16:32:13.132341+0800 内存偏移[12919:415236] value = 3
2021-06-17 16:32:13.132418+0800 内存偏移[12919:415236] value = 4
复制代码
  • 数组的地址就是数组元素中的首地址,即&c&c[0]都是首地址
  • 数组中每个元素之间的地址间隔,根据当前元素的数据类型决定的
  • 数组的元素地址可以通过首地址+n*类型大小方式,这种方式是数组中的元素类型必须相同。
  • 数组元素不相同用首地址+偏移量方式,根据当前变量的偏移值(需要前面类型大小相加)

图解如下
image.png

总结

  • 内存地址就是内存元素的首地址
  • 内存偏移可以根据首地址+ 偏移值方法获取相对应变量的地址

类的分析(isa

探究对象时发现对象的isa指向的是。万物皆对象,类也是个对象,类里面也有一个isa,那么类的isa是指向那个类呢?这个类就是苹果定义的元类

元类

元类是编译器自动生成,苹果为什么要自动生成这么一个类呢?那就来探究下

类对象的内存个数

int main(int argc, char * argv[]) {
    @autoreleasepool {
       Class class1 = [LWPerson class];
       Class class2 = [LWPerson alloc].class;
       Class class3 = object_getClass([LWPerson alloc]);
       Class class4 = [LWPerson alloc].class;
       NSLog(@"\n-%p-\n-%p-\n-%p-\n-%p-",class1,class2,class3,class4);
    }
    return 
}

复制代码
-0x100009510-
-0x100009510-
-0x100009510-
-0x100009510-
复制代码

源码分析:类对象的地址都是一样的,内存中每一个类只有一块内存,和普通的对象有明显的区别

元类探究

image.png

LWPerson类有两个不一样的地址,但是一个类对象只有一个地址。0x0000000100009500LWPerson的类地址,那么0x00000001000094d8这个类地址苹果把它叫做元类

总结

  • 元类是系统编译器自动创建的,和用户没关系
  • 对象的isa指向,类对象的isa指向元类
  • 类的类名和它关联类即元类类名是一样的(只有关联元类才有类名)

isa 走位图

对象元类都有isa, 具体isa走向流程是如何的。 创建一个对象NSObject * obj = [NSObject alloc],结合lldb进行探究
image.png

  • NSObject的对象objisa –> NSObject
  • NSObject类的isa–> 根元类
  • 根元类isa–> 根元类(指向自己)

自定义LWPerson类,创建一个对象LWPerson * person = [LWPerson alloc],结合lldb进行探究

image.png

元类的isa指向根元类,疑问:为什么不是指向NSbOject?注意NSbOject类地址和根元类类地址不一样,所以指向的是根源类

  • LWPerson的对象personisa –> LWPerson
  • LWPerson类的isa–> LWPerson类的元类
  • 元类isa–> 根元类

isa走位流程图

image.png

总结

  • 对象的isa指向类对象
  • 类对象isa指向元类
  • 元类isa指向根元类
  • 根元类isa指向根元类(指向自己)

类、元类、根元类的继承图

首先创建一个LWTeacher类继承LWPerson,探究下它们之间的继承关系。代码如下

int main(int argc, char * argv[]) {
    @autoreleasepool {
    Class tMetaClass = object_getClass(LWTeacher.class);//LWTeacher的元类
    Class tMetaSuperClass = class_getSuperclass(tMetaClass);//LWTeacher的元类的父类
    
    Class pMetaClass = object_getClass(LWPerson.class); //LWPerson的元类
    Class pMeatSuperClass = class_getSuperclass(pMetaClass);//LWPerson的元类的父类
   
    Class nMetaClass = object_getClass(NSObject.class);//NSObject的元类
    Class nSuperClass = class_getSuperclass(NSObject.class);//NSObject的父类
    Class nMetaSuperClass = class_getSuperclass(nMetaClass);//NSObject的元类的父类
    
    NSLog(@"LWTeacher-%p",LWTeacher.class);
    NSLog(@"LWPerson-%p",LWPerson.class);
    NSLog(@"NSObject-%p",NSObject.class);
 
    NSLog(@"%@ - %p - %@ - %p",tMetaClass,tMetaClass,tMetaSuperClass,tMetaSuperClass);
    NSLog(@"%@ - %p - %@ - %p",pMetaClass,pMetaClass,pMeatSuperClass,pMeatSuperClass);
    NSLog(@"%@ - %p - %@ - %p",nMetaClass,nMetaClass,nMetaSuperClass,nMetaSuperClass);
    }
    return 
}
复制代码
2021-06-17 22:12:45.737286+0800 testClass[15587:581908] LWTeacher-0x100009618
2021-06-17 22:12:45.737359+0800 testClass[15587:581908] LWPerson-0x100009528
2021-06-17 22:12:45.737396+0800 testClass[15587:581908] NSObject-0x7fff8deb3118
2021-06-17 22:12:45.737434+0800 testClass[15587:581908] LWTeacher - 0x1000095f0 - LWPerson - 0x100009500
2021-06-17 22:12:45.737497+0800 testClass[15587:581908] LWPerson - 0x100009500 - NSObject - 0x7fff8deb30f0
2021-06-17 22:12:45.737540+0800 testClass[15587:581908] NSObject - 0x7fff8deb30f0 - NSObject - 0x7fff8deb3118
2021-06-17 22:12:45.737583+0800 testClass[15587:581908] - - (null)
复制代码

源码分析 NSObject的父类打印的结果是nilLWTeacher的元类的父类是LWPerson的元类(LWPerson的元类的地址和LWPerson类的地址不一样)。LWPerson的元类的父类是NSObject的元类,NSObject的元类的父类是NSObject(和NSObject类的地址一样)

  • LWTeacher 继承 LWPersonLWPerson 继承 NSObjectNSObject的父类是nil
  • LWTeacher元类 继承 LWPerson元类,LWPerson 继承 根元类根元类继承NSObject

类之间继承流程图

image.png

isa的走位图和继承图

image.png

苹果官方提供isa走位图和继承图

image.png

类结构分析

探究 IOS 底层原理之对象的本质&isa关联类 时,发现isaClass类型的。Class类型是objc_class * objc_class是一个结构体。 所有的Class底层实现都是objc_class。在 objc4-818.2 中全局搜索objc_class 代码如下

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE; // OBJC2 不可用
复制代码

现在正常的都是OBJC2,上面的定义的objc_classOBJC2不可用

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

    //下面是一些方法省略
};
复制代码

源码分析objc_class继承objc_objectobjc_object里面只有一个成员变量isaisa具体是作用已经探究过。下面的三个成员变量具体的作用还无从得知,怎么去探究它呢?类的地址是知道的,那么就根据上面探究过的首地址+偏移值来获取里面的成员变量的地址,然后获取值。但是偏移值需要知道当前变量之前的所有成员变量的大小

  • isa 结构体指针占8字节
  • Class superclass 也是结构体指针占8字节
  • cache_t cache是结构体类型的大小,由结构体内成员变量决定
  • class_data_bits_t bits 知道前面三个成员变量大小,就可以得到bits的地址

只要知道cache_t的内存大小,objc_class的所有的成员变量都可以获取到相应的地址,探究下cache_t内存大小

typedef unsigned long           uintptr_t;

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;   // 4
#if __LP64__
            uint16_t                   _flags;       // 2
#endif
            uint16_t                   _occupied;    // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; //8
    };
    
    //下面是一些方法省略
};
复制代码

cache_t 是结构体类型,有两个成员变量_bucketsAndMaybeMask和一个联合体

  • _bucketsAndMaybeMaskuintptr_t无符长整型占8字节

  • 联合体里面有两个成员变量结构体_originalPreoptCache,联合体的内存大小由成员变量中的最大变量类型决定

  • _originalPreoptCache 是结构体指针占8字节

  • 结构体中有_maybeMask_flags_occupied_maybeMaskuint32_t4字节_flags_occupieduint16_t 各占2字节,结构体大小是8字节

  • cache_t的内存大小是 8+8 或者是8+4+2+2都是16字节

探究类里面的各个成员变量,成员变量的内存地址如下

  • isa的内存地址是首地址,前面的博客已经探究了
  • superclass的内存地址是首地址+0x8
  • cache的内存地址是首地址+0x10
  • bits的内存地址是首地址+0x20

今天主要探究成员变量bitsbits里面存储了哪些重要的信息

image.png

bits.data()应该存储数据,data()的类型是class_rw_t*,源码分析下class_rw_t类型

image.png

class_rw_t是结构体类型,提供了获取属性列表方法列表协议列表的方法。通过实例来验证下方法属性变量是不是在class_rw_t中,在LWPerson类中添加属性方法 以及成员变量

属性探究

image.png
image.png

类中声明的属性nameage存储在属性列表preperty_list_t里,p $7.get(index)获取相应的属性

image.png
p $7.get(2)时报错数组越界,属性列表里只有2个属性,但是声明的变量height存储在哪里?

文章最后补充模块下的变量探究模块,已经对变量的存储进行了探究

方法探究

image.png

  • 对象方法存储在LWPerson类中的方法列表method_list_t
  • 类方法没有在LWPerson的方法列表method_list_t里,类方法放在哪里呢?
  • p $7.get(index)在方法列表中获取不到具体的值,因为method_t中进行了处理,通过bit()获取变量

文章最后补充模块下的类方法探究模块,已经对类方法的存储进行了探究

总结

类中有isasuperclasschchebits 成员变量,在对bits 探究过程中发现bits存储着大家熟悉的属性列表方法列表成员变量列表协议列表等,打开了我们对的认知,后面会继续对类进行探究,敬请期待。

补充

变量探究

属性列表中没有存储变量变量是类中自定义的不应该放在元类中,观察发现class_rw_t还有一个获取class_ro_t *的方法,会不会在class_ro_t中,源码查看class_ro_t的类型

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };

    explicit_atomic<const char *> name;
    
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; //这个名字就好熟悉

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp  
    _swiftMetadataInitializer_NEVER_USE[0];
 };
复制代码

class_ro_t是结构体类型,有一个ivar_list_t * ivars变量。是一个变量列表,按逻辑里面应该存储变量

image.png

  • 变量的底层实现是ivar_t。存储在class_ro_t中的变量列表里
  • 系统会给属性自动生成一个带_属性名变量,存储在class_ro_t中的变量列表里

类方法探究

对象的方法是存储在中,那么类方法可能存储在元类中。按照这个思路探究下

image.png

类方法存储在元类中的方法列表

© 版权声明
THE END
喜欢就支持一下吧
点赞1 分享