OC底层探索之对象原理(下)

lldb命令总结

  • p :类型+引用值+指针地址
  • po: 值(类型+指针地址)
  • p/x : 16进制
  • p/o : 8进制
  • p/t : 2进制
  • p/f: 浮点型
  • x/nsx : x:输出指令 n:n个值 s:以s字节的值 x:值以16进制来表示,如x/4gx p 输出p对象的4个用16进制表示的以8字节对齐成员变量

对象的内存分布

我们都知道申明一个的类的时候,类内部有属性、成员变量、类方法、实例方法,那么哪个因素会影响到我们的对象的内存呢。
首先我们先创建一个类,里面包含一些属性。

@interface CXPersion : NSObject

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *sex;
@property (assign, nonatomic) int age;
@property (assign, nonatomic) short salary;
@property (assign, nonatomic) double height;

@end

OC底层探索之对象原理(上)我们探索了结构体的内存分配,NSObject本质也是一个结构体,里面内部包含一个isa指针,参照上一章节的结构体内存章节。
isa :8字节 [0, 7] 需要注意的是isa是NSObject的一个成员变量
name :8字节 带*号是一个指针类型,指针类型的是8字节[8,15]
sex :8字节[16,31]
age :4字节[32,35]
salary : 2字节[36,37]
height : 8字节[40 47]
所以系统分配对象内存是以16字节对齐的,所以CXPersion一共分配的是48字节,验证一下,输出是48。

验证对象的内存

那么就有疑问了,对象是怎么存储数据的呢?
参考lldb命令,打上断点在控制台输入x/6gx,输出p的6个成员变量,只输出了5个对象的指针地址。第一个值是isa的值,我们分别打印其余的值,看看结果。
x/6gx p

打印name、sex、age、height、salary的值


打印值

我们发现age和height被自动合成了一个8字节对象,这是苹果对对象的成员变量进行了优化重排。当然目前也可以得到一个结论:对象 = isa + ivar。
我们在验证一下,加入一个char类型的,看看对象内存是多少。添加weight,2字节,[48, 49],由于系统给对象分配内存是16字节对齐,所以系统会给p分配64字节。

@interface CXPersion : NSObject

@property (copy, nonatomic) NSString *name; // 8 字节
@property (copy, nonatomic) NSString *sex; // 8 字节
@property (assign, nonatomic) int age; // 4 字节
@property (assign, nonatomic) short height; // 2 字节
@property (assign, nonatomic) double salary; // 8 字节
@property (assign, nonatomic) short weight; // 2 字节
@end

验证一下。凉凉,怎么是48字节,通过分析,系统帮我们优化了,重排了对象的成员变量,把age、height、weight加到了第二个值后面,所以是48字节。


加入weight后对象的内存

成员变量会影响成员变量的内存吗?
添加name,sex 2个成员变量

@interface CXPersion : NSObject
{
    NSString *name; // 8字节
    NSString *sex; // 8字节
}
@end

@implementation CXPersion
- (instancetype)init {
    if (self = [super init]) {
        name = @"cx";
        sex = @"boy";
    }
    return self;
}
@end

分析一下:
isa :8字节 [0, 7]
name :8字节[8,15]
sex :8字节[16,23]
所以系统会给p对象分配32字节内存,验证一下


成员变量

实例方法和类方法会影响对象的内存?
给类添加2个方法并实现。

@interface CXPersion : NSObject
- (void)test ;
+ (void)test ;
@end

运行起来:
输出16,因为对象本身有isa,至少是8字节,系统最少会分配16字节的内存。所以说实例方法和类方法不会影响对象的内存。


方法是否影响对象内存

父类会影响子类的内存吗?
首先我们先申明2个类,子类和父类只包含成员变量,CXTeacher:CXPersion

@interface CXPersion : NSObject
{
    @public
    NSString *name; // 8字节
    NSString *sex; // 8字节
    int age; // 4 字节
    short height; // 2字节
}
@interface CXTeacher : CXPersion
{
    @public
    short height; // 2字节
}
@end

可以得到:p对象和t对象的内存空间是32。


子类父类都是成员变量

调整父类成员变量的位置,运行

@interface CXPersion : NSObject
{
    @public
    NSString *name; // 8字节
    NSString *sex; // 8字节
    short height; // 2字节
    int age; // 4 字节
}
@end

可以得到:p对象的内存空间是32,t对象的内存空间是48。
结论:如果子类父类都是有成员变量构成,父类成员变量顺序可能会影响到子类对象的内存大小。


调整父类成员变量

把子类和父类的成员变量变成属性,运行

@interface CXPersion : NSObject

@property (copy, nonatomic) NSString *name; // 8 字节
@property (copy, nonatomic) NSString *sex; // 8 字节
@property (assign, nonatomic) int age; // 4 字节
@property (assign, nonatomic) short height; // 2字节

@end

@interface CXTeacher : CXPersion

@property (assign, nonatomic) short weight;

@end

可以得到:p对象的内存空间是32,t对象的内存空间是48。


age在前

调整父类属性的位置,运行

@interface CXPersion : NSObject

@property (copy, nonatomic) NSString *name; // 8 字节
@property (copy, nonatomic) NSString *sex; // 8 字节
@property (assign, nonatomic) short height; // 2字节
@property (assign, nonatomic) int age; // 4 字节

@end

可以得到:p对象的内存空间是32,t对象的内存空间是48。


age在后

结论:如果子类父类都是有属性构成,父类成员变量顺序不会影响到子类对象的内存大小,当然子类不会对父类的数据结构产生影响。

位域&联合体

声明一个结构体和结构体位域,struct1的内存大小是4,struct2的内存大小是4吗?运行一下

struct CXStruct1 { // 结构体
    char a;
    char b;
    char c;
    char d;

}struct1; 

struct CXStruct2 { // 结构体位域
    char a : 1;
    char b : 1;
    char c : 1;
    char d : 1;

}struct2;

我们发现结构体位域的内存大小不是4,而是1,因为结构体位域对结构体内存的进行了优化,本来1个char需要1字节(8个比特位),char a:1表示只需要1个比特位来对a的值进行存储,所以实际a只需要1个比特位内存就行了,所以struct2只需要4个比特位内存就行了,但是内存是以字节来来计算的,所以struct2至少需要1字节内存。

结构体位域

位域所需的比特位是不能够大于类型的,1字节=8个比特位。比如char a:9,这样系统会报错,因为1字节<9个比特位,我们不能用9个比特位来存储char类型。1字节存储一个位域但是不够存储下一个位域的时候,就会用新的1字节来存储下一个位域。
申明一个struct3,分析可以得到struct3需要4字节的内存

struct CXStruct3 {
    char a : 7; // 7  需要1字节 内存
    char b : 2; // 7+2 > 8 需要新的字节 总需要2字节内存
    char c : 7; // 2+7 > 8 需要新的字节 总需要3字节内存
    char d : 2; // 7+2 > 8 需要新的字节 总需要4字节内存

}struct3;

验证一下,struct3需要4字节的内存


struct3位域

申明一个联合体

union CXUnion1 {
    char *name;
    short height;
    int age;
}union1;

探索联合体,断点到union1.height = 180.00这里,我们发现union1.name有有效值,其余的都是非法值。

联合体探索

断点到union1.age = 18这里,我们发现union1.height有有效值,其余的都是非法值。
!联合体探索](https://upload-images.jianshu.io/upload_images/16829437-cd0ecd925da9e13e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
断点过union1.age = 18,我们发现union1.age有有效值,其余的都是非法值。分别打印&union1, &union1.name, &union1.height, &union1.age

NSLog(@"%p,%p,%p,%p", &union1, &union1.name, &union1.height, &union1.age);
NSLog(@"struct3---%lu", sizeof(union1));
联合体探索

总结:
结构体(struct):所有变量是“共存”的,优点是“有容乃⼤”,全⾯;缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全分配。
位域:使用位域(比特位)存储,可以大大的优化内存。缺点:需要注意的是位域不能大于所修饰变量的大小。
联合体(union):各变量是“互斥”的,缺点就是不够“包容”;但优点是内存使⽤更为精细灵活,也节省了内存空间。

对象的内存分布

OC底层探索之对象原理(上)我们明白了对象是怎么创建的,那么对象是怎么关联到类的呢?
我们还是基于objc-838来进行探索的,对LGPerson进行初始化

初始化 p

我们知道类本身是有一个isa指针的,那么找到_class_createInstanceFromZone函数在其内部initIsa函数前后分别打上2个断点(initInstanceIsa本质也是调用initIsa函数的)。

objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}
_class_createInstanceFromZone函数

运行,到第一个断点,我们可以看到在initInstanceIsa/initIsa函数之前,系统只是给对象开辟了一块内存地址0x0000000100b47fb0。


关联类之前

运行到第二个断点,我们可以看到0x100b47fb0已经关联上LGPerson了。由此可见initIsa函数是关联对象的类的。


关联类之后

initIsa函数只关联对象的类吗?进入initIsa函数。
inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }
    isa = newisa;
}

我们可以看到isa_t 这个是个联合体,我们类地址相关信息主要是ISA_BITFIELD这个结构体。


isa_t结构体

ISA_BITFIELD结构体,不同架构定义是不一样的。

        // __arm64__
        uintptr_t nonpointer        : 1;                                       
        uintptr_t has_assoc         : 1;                                       
        uintptr_t weakly_referenced : 1;                                       
        uintptr_t shiftcls_and_sig  : 52;                                      
        uintptr_t has_sidetable_rc  : 1;                                       
        uintptr_t extra_rc          : 8
       // __arm64
        uintptr_t nonpointer        : 1;                                       
        uintptr_t has_assoc         : 1;                                       
        uintptr_t has_cxx_dtor      : 1;                                       
        uintptr_t shiftcls          : 33; 
        uintptr_t magic             : 6;                                       
        uintptr_t weakly_referenced : 1;                                       
        uintptr_t unused            : 1;                                       
        uintptr_t has_sidetable_rc  : 1;                                       
        uintptr_t extra_rc          : 19
       // __x86_64__
        uintptr_t nonpointer        : 1;                                         
        uintptr_t has_assoc         : 1;                                         
        uintptr_t has_cxx_dtor      : 1;                                         
        uintptr_t shiftcls          : 44; 
        uintptr_t magic             : 6;                                         
        uintptr_t weakly_referenced : 1;                                         
        uintptr_t unused            : 1;                                         
        uintptr_t has_sidetable_rc  : 1;                                         
        uintptr_t extra_rc          : 8
  • nonpointer: 是否开启NONPOINTER isa指针优化,0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等
  • has_assoc: 是否有关联对象
  • has_cxx_dtor:对象是否含有 C++ 或者 Objc 的析构器
  • shiftcls: 类的指针(重点)(arm64:33位,x86_64:44位)
  • magic: 对象是否初始化完成 (arm64:0x16 ,x86_64:0x3b)
  • weakly_referenced:是否为弱引用对象
  • deallocating:对象是否正在释放
  • has_sidetable_rc:是否需要使⽤ sidetable 来存储引⽤计数
  • extra_rc: 对象的引用计数,如果超过extra_rc的内存,可采用sidetable辅助存储引用计数。

通过isa获取类的地址

我们采用使用mpr的模拟器进行探索,是x86_64架构的,对象的类地址是44位的。
p LGPerson.class:打印LGPerson类的内存,输出0x00000001000080e8
x/4gx p:打印对象p的4个成员的对象地址,第一个对象是isa指针地址,输出0x011d8001000080e9
p/x 0x011d8001000080e9 >> 3 << 20 >> 17:由于类地址是44位的,前三位是nonpointer,has_assoc,has_cxx_dtor我们需要去除后三位,>> 3,然后前移20位去除前17位的数据,在后移17位就可以到magic的值,输出是0x00000001000080e8和p LGPerson.class打印的一样,都是 LGPerson类的内存地址。

通过isa找到类地址

当然系统也给我们提供了一个简单的方式,我们可以isa & ISA_MASK就可以得到对象的类地址。

define ISA_MASK        0x00007ffffffffff8ULL

p/x 0x011d8001000080e9 & 0x00007ffffffffff8ULL输出:0x00000001000080e8验证成功。


isa & ISA_MASK

通过isa获取对象的引用计数

初始化LGPerson的一个对象,我们都知道,p的引用计数是1。看一下:


初始化p对象

使用isa指针地址查看引用计数
x/4gx p:获取p的isa指针内存地址。
p/x 0x011d8001000080f1 >> 56:由于我们使用的模拟器,extra_rc是用8个比特位存储的最多可存储255,右移56位既可得到对象的引用计数。
验证:,输出是1。


使用isa指针地址查看引用计数

如果对象的引用计数大于255系统,系统会采用sidetable辅助存储引用计数

你可能感兴趣的:(OC底层探索之对象原理(下))