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辅助存储引用计数
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容