iOS底层探索之类&类的结构分析

准备工作

创建一个LGPerson类并且创建一个LGTeach类继承自LGPerson类

@interface LGPerson : NSObject {
    NSString * hobby;
}
@property (nonatomic, strong) NSArray *array1;

- (void)sayHello;
+ (void)eat;

@end

@implementation LGPerson

- (void)sayHello {  }

+(void)eat {  }

@end


@interface LGTeracher : LGPerson

@end

@implementation LGTeracher

@end

然后在main函数LGPerson创建一个对象p和LGTeacher创建teacher对象如下

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

        LGPerson *p = [LGPerson alloc];
        LGTeracher * teacher = [LGTeracher alloc];
        NSLog(@"%@-----%@",p,teacher);
    }
    return 0;
}

通过lldb调试如下图


lldb指令调试分析

根据调试过程,我们产生了一个疑问:
为什么上图中的p/x 0x001d80010000336d & 0x00007ffffffffff8ULLp/x 0x0000000100003340 & 0x00007ffffffffff8ULL 中的类信息打印出来都是LGTeracher

0x001d80010000336dteacher对象的isa指针地址,其&mask后得到的结果是 创建teacher的类LGTeracher

0x0000000100003340isa中获取的类信息所指的类的isa的指针地址,即 LGTeracher类的类 的isa指针地址,在Apple中,我们简称LGTeracher类的类为 元类

所以,两个打印都是LGTeracher的根本原因就是因为元类导致的

什么是元类?

对象isa指向也是一个对象,可以称为类对象元类就是类对象所属的元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,的归属来自于元类

  • 对象的isa指向类
  • 类的isa指向元类
  • 元类的isa指向根元类(NSObject)
  • 根元类的isa指向本身

类的信息存在几份

通过以下代码验证

//MARK:--- 分析类对象内存 存在个数
void testClassNum(){
    Class class1 = [LGTeracher class];
    Class class2 = [LGTeracher alloc].class;
    Class class3 = object_getClass([LGTeracher alloc]);
    NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);
}
NSLog打印出来的结果如下:
0x100003368-
0x100003368-
0x100003368-
0x100003368

从结果中可以看出,打印的地址都是同一个,所以NSObject只有一份,即NSObject(根元类)在内存中永远只存在一份

isa走向分析

附上经典的isa走位图


isa走位图

isa走位

isa的走向有以下几点说明:

  • 实例对象(Instance of Subclass)的 isa 指向 类(class)

  • 类对象(class) isa 指向 元类(Meta class)

  • 元类(Meta class)的isa 指向 根元类(Root metal class)

  • 根元类(Root metal class)的isa 指向它自己本身,形成闭环,这里的根元类就是NSObject

superclass走位

superclass(即继承关系)的走向也有以下几点说明:

类 之间 的继承关系:

  • 类(subClass)继承自 父类(superClass)

  • 父类(superClass)继承自 根类(RootClass),此时的根类是指NSObject

  • 根类继承自 nil,所以根类NSObject

元类也存在继承,元类之间的继承关系如下:

  • 子类的元类(metal SubClass) 继承自父类的元类(metal SuperClass)

  • 父类的元类(metal SuperClass) 继承自根元类(Root metal Class)

  • 根元类(Root metal Class) 继承于 根类(Root class),此时的根类是指NSObject

【注意】实例对象之间没有继承关系,类之间有继承关系

类的结构分析

引---内存偏移
int arr[4] = {1, 3, 5, 6};
int *p = arr;
        
for (int i=0; i<4; i++) {
    printf("p[%d] == %d\n", i, p[i]);
}
// output
p[0] == 1
p[1] == 3
p[2] == 5
p[3] == 6

通过lldb调试打印

// 1. 打印p指针
p p
(int *) $0 = 0x00007ffeefbff4b0

// 2. 打印数组的地址
p &arr
(int (*)[4]) $10 = 0x00007ffeefbff4b0

// 3. p 指向 arr 的地址
p arr 
(int [4]) $3 = ([0] = 1, [1] = 3, [2] = 5, [3] = 6)

// 4. 打印数组首元素的地址
p/x &arr[0] 等于 arr 的地址
(int *) $5 = 0x00007ffeefbff4b0

// 5. 打印数组首元素的值
p *arr
(int) $11 = 1

// 6. 打印数组后续元素的值, 最好带个括号,易读性强
(lldb) p * (arr + 1)
(int) $13 = 3
(lldb) p * (arr + 3)
(int) $14 = 6

通过数组的首地址,然后拿到偏移量就可以获取到其它的元素

objc_class & objc_object
通过我们之前下载的objc781的源码我们在源码中查找得到:

objc_object

objc_class

总结:
objc_class继承于objc_object,objc_class中有一个公用的isa,所以所有的对象都继承于objc_object,万物皆来源于objc_object
objc_class、objc_object、isa、object、NSObject等的整体的关系,如下图所示

整体关系图
类的结构分析

通过上述源码可知:
isa属性objc_class继承自objc_object,故objc_class含有isa属性,占8个字节。
superclass 属性:Class类型,Class是由objc_class定义的结构体,是一个指针,占8字节
cache属性:简单从类型class_data_bits_t目前无法得知,而class_data_bits_t是一个结构体类型,结构体的内存大小需要根据内部的属性来确定,而结构体指针才是8字节
bits属性:只有首地址经过上面3个属性的内存大小总和的平移,才能获取到bits

  • 计算cache类的内存大小
    进入cachecache_t的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中),代码如下
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic _buckets;
    explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic _maskAndBuckets;
    mask_t _mask_unused;

计算前两个属性的内存大小,有以下两种情况,最后的内存大小总和都是12字节

  • 【情况一】if流程
    buckets类型是struct bucket_t *,是结构体指针类型,占8字节
    maskmask_t类型,而mask_tunsigned int的别名,占4字节
  • 【情况二】elseif流程
    _maskAndBucketsuintptr_t类型,它是一个指针,占8字节
    _mask_unusedmask_t 类型,而 mask_tuint32_t 类型定义的别名,占4字节
    _flagsuint16_t类型,uint16_tunsigned short 的别名,占2个字节
    _occupieduint16_t类型,uint16_tunsigned short的别名,占 2个字节

总结:所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16字节

获取bits
所以有上述计算可知,想要获取bits的中的内容,只需通过类的首地址平移32字节即可

探索 属性列表,即 property_list
通过class_rw_t源码我们查找到以下代码,结构体中有提供相应的方法去获取 属性列表方法列表等,

const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get()->methods;
        } else {
            return method_array_t{v.get()->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get()->properties;
        } else {
            return property_array_t{v.get()->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get()->protocols;
        } else {
            return protocol_array_t{v.get()->baseProtocols};
        }
    }

通过lldb调试如下图:


获取属性列表属性的lldb调试过程

lldb指令解析:

  • p $3.properties()打印出属性数组
  • p $4.list获取属性数组的属性列表
  • p *$5获取属性列表的第一个属性

通过上述的打印看,只打印出属性,没有打印出成员变量。属性与成员变量的区别就是有没有set、get方法,那成员变量存储在哪里?

成员变量列表

通过查看objc_classbits属性中存储数据的类class_rw_t的定义发现,除了methodspropertiesprotocols方法,还有一个ro方法,其返回类型是class_ro_t,通过查看其定义,发现其中有一个ivars属性,我们可以做如下猜测:是否成员变量就存储在这个ivar_list_t类型的ivars属性中呢?

获取成员变量列表属性的lldb调试过程

通过调试得出 ivar_list_t类型的属性中不仅包含了成员变量还包括了我们之前的属性列表。

实例方法列表

通过lldb调试来获取方法列表,步骤如图所示

获取实例方法列表lldb调试流程

通过p $5.methods()获得具体的方法列表的list结构,其中methods也是class_rw_t提供的方法

通过打印p *$7count = 4可知,存储了4个方法,可以通过p $8.get(i)内存偏移的方式获取单个方法,i的范围是0-3

如果在打印 p $8.get(4),获取第五个方法,也会报错,提示数组越界

但是我们发现我们声明的类方法并不存在中,而实例方法存在中,那么类方法会不会存在元类中呢?下面我们探索一下。

类方法列表

获取类方法列表lldb调试流程

通过调试流程我们得以验证我们这之前的猜测是正确的,类方法的存储在元类的bits中 。

总结:

通过{}定义的成员变量,会存储在类的bits属性中,通过bits--> data() -->ro() --> ivars获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量

通过@property定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含属性

类的实例方法存储在类的bits属性中,通过bits --> methods() --> list获取实例方法列表,例如LGPerson类的实例方法instanceMethod就存储在 LGPerson类的bits属性中

类的类方法存储在元类bits属性中,通过元类bits --> methods() --> list获取类方法列表

你可能感兴趣的:(iOS底层探索之类&类的结构分析)