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的值,我们分别打印其余的值,看看结果。
打印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字节。
成员变量会影响成员变量的内存吗?
添加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。
调整父类属性的位置,运行
@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。
结论:如果子类父类都是有属性构成,父类成员变量顺序不会影响到子类对象的内存大小,当然子类不会对父类的数据结构产生影响。
位域&联合体
声明一个结构体和结构体位域,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字节的内存
申明一个联合体
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进行初始化
我们知道类本身是有一个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);
}
运行,到第一个断点,我们可以看到在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_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_MASK就可以得到对象的类地址。
define ISA_MASK 0x00007ffffffffff8ULL
p/x 0x011d8001000080e9 & 0x00007ffffffffff8ULL输出:0x00000001000080e8验证成功。
通过isa获取对象的引用计数
初始化LGPerson的一个对象,我们都知道,p的引用计数是1。看一下:
使用isa指针地址查看引用计数
x/4gx p:获取p的isa指针内存地址。
p/x 0x011d8001000080f1 >> 56:由于我们使用的模拟器,extra_rc是用8个比特位存储的最多可存储255,右移56位既可得到对象的引用计数。
验证:,输出是1。
如果对象的引用计数大于255系统,系统会采用sidetable辅助存储引用计数