iOS类的底层探索(下)

2,382 阅读9分钟

前面我们已经学习了对象相关内容, 如元类根元类,以及superclass的结构与关系等;初步分析了类的结构superclass,cache,class_data_bits_t,class_data_bits_t结构体中提供了data()方法,用于获取class_rw_tclass_rw_t是在类初始化过程中已经被创建了,并且class_rw_t的相关数据来自MachO文件ro数据!简单总结:对象是类的实例,类是元类的实例,方法都存储在各自的类中。

一. iOS类属性重排

1、自定义NYPerson类,不定义任何成员变量和属性

    NYPerson *person = [[NYPerson alloc] init];

    NSLog(@"person对象类型占用内存的大小:%lu",sizeof(person));

    NSLog(@"person对象实际占用的内存的大小:%lu",class_getInstanceSize([person class]));

    NSLog(@"person对象实际分配型内存的大小:%lu",malloc_size((__bridge const void*)(person)));
    
**2022-04-30 20:15:34.888166+0800 Test[3543:74546] person对象类型占用内存的大小:8**

**2022-04-30 20:15:34.888436+0800 Test[3543:74546] person对象实际占用的内存的大小:8**

**2022-04-30 20:15:34.888594+0800 Test[3543:74546] person对象实际分配型内存的大小:16**

实际占用内存大小为8是因为NYPerson继承NSObject,NSObject自带Class 类型isa成员变量, 本质是isa指针,占8个字节。 实际分配内存为16是因为OC对象内存开辟遵循16字节对齐。在ios对象的底层探索(下)中有详细介绍.

2、给NYPerson添加sex、heigt属性,看打印结果:

@interface NYPerson : NSObject

@property (nonatomic, assign) BOOL sex;

@property (nonatomic, assign) double height;

@end

**2022-04-30 20:28:51.680828+0800 Test[3963:85673] person对象类型占用内存的大小:8**

**2022-04-30 20:28:51.681218+0800 Test[3963:85673] person对象实际占用的内存的大小:24**

**2022-04-30 20:28:51.681941+0800 Test[3963:85673] person对象实际分配型内存的大小:32**

person对象实际占用的内存情况: 0~7存储isa指针,第8位存储sex,16~23存储height,所以实际占用为8的倍数24,实际分配为16的倍数32。

3、给NYPerson添加sex、heigt属性,看打印结果:

@interface NYPerson : NSObject

@property (nonatomic, assign) BOOL sex;

@property (nonatomic, assign) double height;

@property (nonatomic, assign) int age;

@property (nonatomic, assign) short weight;

@end

**2022-04-30 20:44:03.189658+0800 Test[4275:95130] person对象类型占用内存的大小:8**

**2022-04-30 20:44:03.191476+0800 Test[4275:95130] person对象实际占用的内存的大小:24**

**2022-04-30 20:44:03.194769+0800 Test[4275:95130] person对象实际分配型内存的大小:32**

疑问:如果实际占用内存按照自定义属性的顺序来计算的话,那么实际占用内存应该是 0~7存储isa指针,第8位存储sex,16~23位存储height,24~27位存储age,28~29位存储weight,然后按8字节对齐应该是32才对,但是这里还是24,这就证明编译器肯定对内存进行过优化。所以我们定义属性的时候不用管定义的顺序,编译器会自动帮我们进行优化。

小结: 编译器会对属性顺序进行优化,从而节省内存

4、给属性赋值,通过lldb调试证明

image.png

当我们向0x0000001200140001地址找出数据时,发现是看不懂的,这里无法找出值的原因是苹果中针对sex、age、weight属性的内存进行了重排因为age类型占4个字节,sex和weight类型分别占1个字节,2个字节,通过4+2+1的方式,按照8字节对齐,不足补齐的方式存储在同一块内存中。注意:浮点型需要用p/f查看

5、只有成员变量时,还会不会自动帮我们排序呢?

image.png

将属性改为成员变量,再次打印结果会,发现编译器不会自动帮我们进行优化,而是按照实际成员变量顺序来计算实际占用内存的大小。

小结:使用成员变量,编译器并不会自动帮我们进行内存优化,需要自己去排序成员变量,而使用属性,编译器会自动帮我们进行内存优化,从而节约内存。

 6、既有成员变量又有属性时

@interface NYPerson : NSObject

{

    double sheight;

    BOOL scout;
}

@property (nonatomic, assign) BOOL sex;

@property (nonatomic, assign) double height;

@property (nonatomic, assign) int age;

@property (nonatomic, assign) short weight;

@end

**2022-04-30 21:13:41.448553+0800 Test[4968:116618] person对象类型占用内存的大小:8**

**2022-04-30 21:13:41.450152+0800 Test[4968:116618] person对象实际占用的内存的大小:32**

**2022-04-30 21:13:41.450493+0800 Test[4968:116618] person对象实际分配型内存的大小:32**

----------------------------------------------------------------------------------------
@interface NYPerson : NSObject

{

    BOOL scout;

    double sheight;

}

@property (nonatomic, assign) BOOL sex;

@property (nonatomic, assign) double height;

@property (nonatomic, assign) int age;

@property (nonatomic, assign) short weight;

@end
**2022-04-30 21:14:16.863168+0800 Test[4991:117397] person对象类型占用内存的大小:8**

**2022-04-30 21:14:16.863454+0800 Test[4991:117397] person对象实际占用的内存的大小:40**

**2022-04-30 21:14:16.863842+0800 Test[4991:117397] person对象实际分配型内存的大小:48**

小结:从上面代码可看出,当成员变量和属性同事存在时,会先给成员变量按顺序分配内存,然后再给属性自动优化分配内存。

二. 类结构class_ro_t,class_rw_t,class_rw_ext_t的解析

1、class_ro_t

class_ro_t存储了当前类在编译期就已经确定的属性方法以及遵循的协议,里面是没有分类的方法。那些运行时添加的方法将会存储在运行时生成的class_rw_t中。

ro即表示read only,是无法进行修改的。

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

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

2、class_rw_t

ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t中:

// 可读可写
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro; // 指向只读的结构体,存放类初始信息

    /*
     这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
     methods中,存储 method_list_t ----> method_t
     二维数组,method_list_t --> method_t
     这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
     */
    method_array_t methods; // 方法列表(类对象存放对象方法,元类对象存放类方法)
    property_array_t properties; // 属性列表
    protocol_array_t protocols; //协议列表

    Class firstSubclass;
    Class nextSiblingClass;
    
    //...
    }


class_rw_t生成在运行时,在编译期间,class_ro_t结构体就已经确定,objc_class中的bitsdata部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtimerealizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。 类的realizeClass运行之前:

image.png 类的realizeClass运行之后: image.png 细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容剪切过去,然后再将当前类的分类的这些属性、方法等剪切到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容

3、class_rw_ext_t

class_rw_ext_t可以减少内存的消耗。苹果在wwdc2020⾥⾯说过,只有⼤约10%左右的类需要动 态修改。所以只有10%左右的类⾥⾯需要⽣成class_rw_ext_t这个结构体。这样的话,可以节约很 ⼤⼀部分内存。

image.png class_rw_ext_t⽣成的条件:

第⼀:⽤过runtime的Api进⾏动态修改的时候。

第⼆:有分类的时候,且分类本类都为⾮懒加载类的时候。实现了+load⽅法即为⾮懒加载类。

class_ro_t,class_rw_t,class_rw_ext_t,他们之间的关系图:

image.png

三.通过runtime的api探索类的数据结构

1、获取类的成员变量

-(void)ny_class_copyIvarList:(Class)pClass {

    unsigned int  outCount = 0;

    Ivar *ivars = class_copyIvarList(pClass, &outCount);

    for (int i = 0; i < outCount; i ++) {

        Ivar ivar = ivars[i];

        const char *cName =  ivar_getName(ivar);

        const char *cType = ivar_getTypeEncoding(ivar);

        NSLog(@"name = %s type = %s",cName,cType);

    }

    free(ivars);

}

2、获取类的属性

-(void)ny_class_copyPropertyList:(Class)pClass {

    unsigned int outCount = 0;

    objc_property_t *perperties = class_copyPropertyList(pClass, &outCount);

    for (int i = 0; i < outCount; i++) {

        objc_property_t property = perperties[i];

        const char *cName = property_getName(property);

        const char *cType = property_getAttributes(property);

        NSLog(@"name = %s type = %s",cName,cType);

    }

    free(perperties);

}

3、获取类的方法

-(void)lg_class_copyMethodList:(Class)pClass {

    unsigned int outCount = 0;

    Method *methods = class_copyMethodList(pClass, &outCount);

    for (int i = 0; i < outCount; i++) {

        Method method = methods[i];

        NSString *name = NSStringFromSelector(method_getName(method));

        const char *cType = method_getTypeEncoding(method);

        NSLog(@"name = %@ type = %s",name,cType);

    }

    free(methods);

}

总结

1.OC对象里面属性在编译时,编译器会对属性顺序进行优化,从而节省内存。而使用成员变量时,编译器会安装,我们书写的顺序进行内存计算对齐,不会进行优化。

2.class_ro_t存储了当前类在编译期就已经确定的属性方法以及遵循的协议,里面是没有分类的方法

2.class_rw_t是在运⾏的时候⽣成的,类⼀经使⽤就会变成class_rw_t,它会先将class_ro_t的内 容"拿"过去,然后再将当前类的分类的这些属性⽅法协议等拷⻉到class_rw_t⾥⾯其中class_ro_t *ro 指向class_ro_t。它是可读写的。

4.class_rw_ext_t可以减少内存的消耗。class_rw_ext_t⽣成的条件:

(1)⽤过runtime的Api进⾏动态修改的时候。

(2)有分类的时候,且分类和本类都为⾮懒加载类的时候。实现了+load⽅法即为⾮懒加载类。