問題一:一個NSObject對象占用多少內(nèi)存宪哩?
一. 分析NSObject
1. 通過源碼分析
我們平時編寫的Objective-C代碼谷徙,底層實現(xiàn)其實都是C\C++代碼
所以O(shè)bjective-C的面向?qū)ο蠖际腔贑\C++的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的
思考:Objective-C的對象讶请、類主要是基于C\C++的什么數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的蘸吓?
答案: 結(jié)構(gòu)體
如何將Objective-C代碼轉(zhuǎn)換為C\C++代碼?
首先創(chuàng)建一個命令行項目,只寫下面一句代碼:
NSObject *obj = [[NSObject alloc] init];
cd到main.m文件對應(yīng)的文件夾笛园,執(zhí)行以下指令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 輸出的CPP文件
解釋:
- (使用xcode) (指定sdk跑在iOS平臺上) (用clang編譯器) (指定架構(gòu)arm64) (重寫objc文件) (OC源文件名稱) (輸出) (輸出文件名稱)
- 如果需要鏈接其他框架涂邀,使用-framework參數(shù)瘟仿。比如-framework UIKit
- 如果將C++文件添加到項目中,需要將C++文件移除編譯比勉,否則運(yùn)行報錯
- 在使用clang轉(zhuǎn)換OC為C++代碼時劳较,可能會遇到以下問題:
cannot create __weak reference in file using manual reference
解決方案:支持ARC、指定運(yùn)行時系統(tǒng)版本浩聋,比如:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
在終端執(zhí)行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp指令之后我們就把main.m文件轉(zhuǎn)換成C++文件了,打開 main-arm64.cpp文件,搜索int main(int,可以發(fā)現(xiàn)main函數(shù)被重寫成如下代碼
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
}
return 0;
}
再搜索IMPL,發(fā)現(xiàn)如下結(jié)構(gòu)體,這就是底層通過C++定義的NSObject的結(jié)構(gòu)體
// NSObject Implementation NSObject底層實現(xiàn)
struct NSObject_IMPL {
Class isa; // 指針在64位系統(tǒng)占8個字節(jié) 在32位系統(tǒng)占4字節(jié)
};
對比OC對于NSObject的定義,發(fā)現(xiàn)兩者其實是一樣的
// NSObject定義
@interface NSObject {
Class isa;
}
@end
驗證了Objective-C的對象观蜗、類主要是基于C\C++的結(jié)構(gòu)體實現(xiàn)的
點(diǎn)進(jìn)入Class,發(fā)現(xiàn)isa其實就是一個指向結(jié)構(gòu)體的指針
// 指針
typedef struct objc_class *Class;
既然是指針, 指針在64位系統(tǒng)占8個字節(jié),在32位系統(tǒng)占4字節(jié)
現(xiàn)在我們就明白了,下面一句代碼在內(nèi)存中做了什么事了
NSObject *obj = [[NSObject alloc] init];
首先alloc之后,系統(tǒng)會給這個結(jié)構(gòu)體分配內(nèi)存,由于結(jié)構(gòu)體里面只有一個isa指針,所以isa的內(nèi)存地址就是結(jié)構(gòu)體的內(nèi)存地址,假設(shè)isa指針內(nèi)存地址為: 0x100400110,那么整個結(jié)構(gòu)體的內(nèi)存地址也是這個,整個NSObject對象的內(nèi)存地址也是這個
然后再用一個obj指針指向這個內(nèi)存地址(這個obj指針里面存放的就是這個對象的地址值)
內(nèi)存關(guān)系圖:
回到文章剛開始的問題,一個NSObject對象占用多少內(nèi)存?
可能你會說是8個,其實是16個字節(jié)
先了解兩個函數(shù):
size_t class_getInstanceSize(Class _Nullable cls)
獲取實例對象的成員變量所占用內(nèi)存大小(內(nèi)存對齊后的) -> 其實就是實例對象至少占用的內(nèi)存大小
size_t malloc_size(const void *ptr)
獲取指針?biāo)赶騼?nèi)存的大小 -> 其實就是實例對象實際占用的內(nèi)存大小
分別導(dǎo)入兩個函數(shù)對應(yīng)的頭文件
#import <objc/runtime.h>
#import <malloc/malloc.h>
打印:
NSObject *obj = [[NSObject alloc] init]; // 16個字節(jié)
// 獲得NSObject實例對象的成員變量所占用的大小 打印 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// 獲得obj指針?biāo)赶騼?nèi)存的大小 打印 16
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
第一個打印8,第二個打印16
回到問題一,我們不難發(fā)現(xiàn)問題的答案應(yīng)該是16
總結(jié):一個NSObject對象占用16字節(jié)的內(nèi)存
系統(tǒng)分配了16個字節(jié)給NSObject對象(通過malloc_size函數(shù)獲得)
但NSObject對象內(nèi)部只使用了8個字節(jié)的空間(64bit環(huán)境下衣洁,可以通過class_getInstanceSize函數(shù)獲得)
為什么通過class_getInstanceSize獲取的是8呢?
其實objc底層好多源碼是開源的,我們在https://opensource.apple.com/tarballs/搜索objc,點(diǎn)擊objc4文件夾進(jìn)去,下載一個最新的(數(shù)字最大的)
解壓之后我們就能查看runtime源碼了,打開項目搜索class_getInstanceSize
//獲得的是內(nèi)存對齊后的大小 aligned對齊
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
進(jìn)去alignedInstanceSize,可以看出返回的是ivar成員變量的內(nèi)存大小,所以打印的才是8
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
為什么我們需要8字節(jié),系統(tǒng)給我們分配16字節(jié)呢?
現(xiàn)在我們看看alloc內(nèi)部是怎么實現(xiàn)的,其實alloc內(nèi)部調(diào)用的是allocWithZone,搜索_objc_rootAllocWithZone函數(shù)
①進(jìn)入class_createInstance -> _class_createInstanceFromZone
②在_class_createInstanceFromZone里面我們發(fā)現(xiàn)有一個函數(shù) obj = (id)calloc(1, size),其實這個calloc就是實際分配內(nèi)存的函數(shù),它傳入一個參數(shù)size
③進(jìn)入獲取size的函數(shù) size_t size = cls->instanceSize(extraBytes)
size_t instanceSize(size_t extraBytes) {
//size_t size = class_getInstanceSize(Class) + extraBytes;)
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
可以看出,如果size<16,size=16
現(xiàn)在我們就明白了墓捻,為什么OC對象至少占用16個字節(jié)了,因為系統(tǒng)的硬性規(guī)定坊夫。
觀察上面的代碼,發(fā)現(xiàn)上面的代碼也調(diào)用了class_getInstanceSize 的內(nèi)部方法alignedInstanceSize
所以,size_t size = alignedInstanceSize() + extraBytes其實就相當(dāng)于size_t size = class_getInstanceSize(Class) + extraBytes
2. 通過Xcode的viewMemory查看對象內(nèi)存結(jié)構(gòu)
下面我們換個方式砖第,使用Xcode的viewMemory查看obj對象內(nèi)存結(jié)構(gòu)
- 打斷點(diǎn),點(diǎn)擊obj,獲取到打印的內(nèi)存
- 進(jìn)入viewMemory
- 輸入地址
- 查看內(nèi)存
圖中是使用16進(jìn)制的,一個16進(jìn)制位代表4個二進(jìn)制位,兩個16進(jìn)制位代表8個二進(jìn)制位 (一個字節(jié)占用8個二進(jìn)制位大小),所以兩個16進(jìn)制位代表一個字節(jié)
不明白的可參考:為什么一個字節(jié)占8個二進(jìn)制
可以發(fā)現(xiàn)前8個字節(jié)有值,后8個字節(jié)為0,可以猜想系統(tǒng)分配內(nèi)存的時候先清零,先分配了16個字節(jié),但是我只用8個,所以把前8個給用了,后8個字節(jié)還空著
3. 使用lldb指令查看內(nèi)存
除了使用viewMemory查看內(nèi)存,還可以使用lldb指令查看內(nèi)存:
常用指令:
print/p :打印
printobject/po :打印對象
讀取內(nèi)存:
memory read/打印數(shù)量+格式+字節(jié)數(shù) 內(nèi)存地址
x/打印數(shù)量+格式+字節(jié)數(shù) 內(nèi)存地址 (x是memory read簡寫)
例如: x/3xw 0x10010
(打印幾串)(用什么進(jìn)制)(每一串多少字節(jié))
修改內(nèi)存中的值:
memory write 內(nèi)存地址 數(shù)值
memory write 0x0000010 10
格式:
x是16進(jìn)制,f是浮點(diǎn)环凿,d是10進(jìn)制
字節(jié)數(shù):
b:byte 1字節(jié)梧兼,h:half word 2字節(jié)
w:word 4字節(jié),g:giant word 8字節(jié)
例如我們讀取上面的obj地址:
(lldb) p obj
(NSObject *) $0 = 0x0000000100766be0
(lldb) po obj
<NSObject: 0x100766be0>
(lldb) memory read 0x100766be0
0x100766be0: 41 91 e9 97 ff ff 1d 00 00 00 00 00 00 00 00 00 A...............
0x100766bf0: c0 6c 76 00 01 00 00 00 00 6f 76 00 01 00 00 00 .lv......ov.....
(lldb) memory read/3xg 0x100766be0
0x100766be0: 0x001dffff97e99141 0x0000000000000000
0x100766bf0: 0x0000000100766cc0
(lldb) x 0x100766be0
0x100766be0: 41 91 e9 97 ff ff 1d 00 00 00 00 00 00 00 00 00 A...............
0x100766bf0: c0 6c 76 00 01 00 00 00 00 6f 76 00 01 00 00 00 .lv......ov.....
(lldb) x/3xg 0x100766be0
0x100766be0: 0x001dffff97e99141 0x0000000000000000
0x100766bf0: 0x0000000100766cc0
(lldb) x/4xw 0x100766be0
0x100766be0: 0x97e99141 0x001dffff 0x00000000 0x00000000
(lldb) x/4dg 0x100766be0
0x100766be0: 8444247555019073
0x100766be8: 0
0x100766bf0: 4302728384
0x100766bf8: 4302728960
(lldb)
就按照x/(打印幾串)(用什么進(jìn)制)(每一串多少字節(jié))格式來就可以了
可以發(fā)現(xiàn)打印結(jié)果和使用viewMemory是一樣的,但是x 0x100766be0和x/3xg 0x100766be0打印的內(nèi)存信息展示方式卻是相反的,這是因為iOS都是小端模式,是從右往左讀取的,所以,內(nèi)存中分布是:41 91 e9 97 ff ff 1d 讀取出來就是:0x001dffff97e99141
修改內(nèi)存:
從上面可以看出,第一個字節(jié)的內(nèi)存地址是0x100766be0,我們想修改第十個字節(jié)的數(shù)據(jù)為9,指令為memory write 0x100766be9 9
(lldb) x 0x100766be0
0x100766be0: 41 91 e9 97 ff ff 1d 00 00 00 00 00 00 00 00 00 A...............
0x100766bf0: c0 6c 76 00 01 00 00 00 00 6f 76 00 01 00 00 00 .lv......ov.....
(lldb) memory write 0x100766be9 9
(lldb) x 0x100766be0
0x100766be0: 41 91 e9 97 ff ff 1d 00 00 09 00 00 00 00 00 00 A...............
0x100766bf0: c0 6c 76 00 01 00 00 00 00 6f 76 00 01 00 00 00 .lv......ov.....
(lldb)
可以發(fā)現(xiàn)第十個字節(jié)被改成09了
二. 分析Student
上面我們分析是最簡單的NSObject類智听,現(xiàn)在我們定義一個有兩個成員變量的Student類羽杰,如下捷泞,查看它的內(nèi)存情況拓劝。
首先我們要知道指針占用8字節(jié),int類型數(shù)據(jù)占用4字節(jié)今豆。
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
使用上面相同的方法重寫為C++文件,在文件中搜索Student_IMPL,結(jié)果如下:
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
上面我們已經(jīng)知道NSObject_IMPL結(jié)構(gòu)體就是NSObject的底層定義,里面只有一個isa指針
//NSObject的實現(xiàn) 這個結(jié)構(gòu)體只占用8個字節(jié),只不過硬性分配給他16個字節(jié)
struct NSObject_IMPL {
Class isa;
};
所以Student類內(nèi)部的底層的結(jié)構(gòu)體實現(xiàn)其實就是:
struct Student_IMPL {
//struct NSObject_IMPL NSObject_IVARS;
Class isa;
int _no;
int _age;
};
賦值:
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
現(xiàn)在Student對象內(nèi)部的內(nèi)存分布情況我們就很明白了
由于這個結(jié)構(gòu)體中的第一個值的內(nèi)存地址是0x100400110 所以整個結(jié)構(gòu)體的內(nèi)存地址也是0x100400110
,所以Student對象的內(nèi)存地址也是0x100400110,所以stu指針里面存放的地址也是0x100400110
再次驗證對象本質(zhì)就是結(jié)構(gòu)體:
下面用結(jié)構(gòu)體指針指向stu,再通過結(jié)構(gòu)體直接訪問成員變量,訪問成功,說明對象本質(zhì)就是結(jié)構(gòu)體
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
//no is 4, age is 5
同樣我們使用viewMemory驗證,發(fā)現(xiàn)4和5的確在內(nèi)存里面同樣使用上面兩個函數(shù)打印
NSLog(@"%zd", class_getInstanceSize([Student class]));
//16
NSLog(@"%zd", malloc_size((__bridge const void *)stu));
//16
可以發(fā)現(xiàn)Student對象里的成員變量一共占用16個字節(jié)莉测,stu指針指的內(nèi)存也是占用16個字節(jié)颜骤,其中前面8個字節(jié)放isa,后面4字節(jié)個放_no悔雹,最后4個字節(jié)放_age复哆。
它們在內(nèi)存中是連續(xù)的,內(nèi)存圖如下:
三. 分析Person和Student
我們定義更復(fù)雜的類,如下
// Person
@interface Person : NSObject
{
@public
int _age;
}
@end
//Student
@interface Student : Person
{
int _no;
}
@end
Student繼承于Person
Person *person = [[Person alloc] init];
person->_age = 20;
Student *stu = [[Student alloc] init];
stu->_no = 10;
class_getInstanceSize 獲取的是對齊后的內(nèi)存大小
NSLog(@"person - %zd", class_getInstanceSize([Person class])); //16
NSLog(@"person - %zd", malloc_size((__bridge const void *)person)); //16
NSLog(@"stu - %zd", class_getInstanceSize([Student class])); //16
NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu)); //16
可以發(fā)現(xiàn)打印都是16,分析:
對于person,底部代碼為:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8
int _age; // 4
}; // 一共16字節(jié)
就算沒OC源碼里面的至少為16字節(jié)的規(guī)定,由于結(jié)構(gòu)體的內(nèi)存對齊:結(jié)構(gòu)體的大小必須是最大成員大小的倍數(shù) 從這個角度看也是16字節(jié)腌零。
對于Student,底部源碼是:
struct Student_IMPL {
struct Person_IMPL Person_IVARS; // 16
int _no; // 4
}; // 一共16字節(jié)
雖然Person_IMPL占用16字節(jié),但是他有4字節(jié)空出來的,所以_no正好放在那里
他們內(nèi)存結(jié)構(gòu)圖如下:
當(dāng)然你也可以使用viewMemory和lldb查看內(nèi)存情況,這里就省略了
如果給Person添加一個屬性,內(nèi)存中是什么樣呢?
@property (nonatomic, assign) int height;
查看C++文件,看出底層是這樣的
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; //8字節(jié)
int _no; //4字節(jié)
int _height //4字節(jié)
}; //一共16字節(jié)
可以看出實例對象的內(nèi)存中多了一個_height梯找,沒有setter和getter方法。
setter和getter方法為什么不和成員變量放一塊呢?
因為方法一份就夠了,多個對象都可以調(diào)用,沒必要放實例對象的內(nèi)存中,其實方法放到類對象和方法列表里面)
四. 解答最后一個疑問益涧,引入iOS的內(nèi)存對齊
創(chuàng)建如下類
@interface MJPerson : NSObject
{
int _age;
int _height;
int _no;
}
@end
通過上面的學(xué)習(xí),我么你很容易知道它底層是這樣的
struct NSObject_IMPL
{
Class isa;
};
struct MJPerson_IMPL
{
struct NSObject_IMPL NSObject_IVARS; // 8
int _age; // 4
int _height; // 4
int _no; // 4
}; // 計算結(jié)構(gòu)體大小锈锤,按照結(jié)構(gòu)體內(nèi)存對齊,24
然后我們按照最少16字節(jié),結(jié)構(gòu)體的內(nèi)存對齊要是8的倍數(shù)來分析,這個結(jié)構(gòu)體至少需要占用24字節(jié)
接下來我們打印:
MJPerson *p = [[MJPerson alloc] init];
NSLog(@"%zd", sizeof(struct MJPerson_IMPL)); // 24
NSLog(@"%zd %zd",
class_getInstanceSize([MJPerson class]), // 24
malloc_size((__bridge const void *)(p))); // 32
可以發(fā)現(xiàn),結(jié)構(gòu)體實際需要24字節(jié),但是系統(tǒng)卻給Person對象32字節(jié),為什么呢?
首先,我們還是查看源碼,按照剛開始的查看alloc底層方法調(diào)用的順序,我們會發(fā)現(xiàn)如下兩個熟悉的方法
size_t size = cls->instanceSize(extraBytes);
obj = (id)calloc(1, size);
按照剛開始我們的分析instanceSize其實就相當(dāng)于class_getInstanceSize,所以它返回的就是24,但是在calloc函數(shù)里面把24傳進(jìn)入怎么就變成32了呢?
你可能會說再查看calloc底層不就好了,calloc的底層在liamalloc庫里面,也是在https://opensource.apple.com/tarballs/里面下載,但是分析太麻煩了闲询,我就直接說結(jié)論了久免。
結(jié)論:
因為ios系統(tǒng)也有內(nèi)存對齊的概念,內(nèi)存必須是16的倍數(shù)扭弧,就算你只需要24字節(jié)阎姥,傳給我24,我也會傳給你32字節(jié)鸽捻。
這也解釋了呼巴,為什么NSObject里面只有一個isa指針(占8字節(jié)),但是還是給他16字節(jié)的原因了御蒲。
小補(bǔ)充: sizeof
- sizeof和class_getInstanceSize都是返回至少需要多少內(nèi)存,而malloc_size返回的是實際需要的
- 但是他們也有不同點(diǎn):sizeof傳進(jìn)來一個類型進(jìn)來,我告訴你類型有多大,比如int,sizeof是個運(yùn)算符,不是函數(shù),所以上面我們使用sizeof打印的時候需要傳入結(jié)構(gòu)體NSLog(@"%zd", sizeof(struct MJPerson_IMPL)); // 24
就算你這樣寫
MJPerson *p = [[MJPerson alloc] init];
NSLog(@"%zd", sizeof(p)); //8
就算你把p傳進(jìn)去,它打印的也是8,因為你把一個指針(占用8字節(jié))傳進(jìn)去了,因為sizeof是個運(yùn)算符,所以在編譯的時候就已經(jīng)確定是8了,就相當(dāng)于
MJPerson *p = [[MJPerson alloc] init];
NSLog(@"%zd", 8); //8
- class_getInstanceSize傳一個類進(jìn)來,我告訴你最終創(chuàng)建的實例大小,是個函數(shù)
總結(jié):
- Objective-C的對象衣赶、類主要是基于C\C++的結(jié)構(gòu)體實現(xiàn)的. NSObject對象底層是個結(jié)構(gòu)體,結(jié)構(gòu)體內(nèi)部只有一個isa指針。
- 一個指針占8個字節(jié)厚满,所以結(jié)構(gòu)體實際需要8字節(jié)府瞄,但是一個NSObject對象卻占用16個字節(jié)(因為iOS的內(nèi)存對齊或者說系統(tǒng)規(guī)定至少占用16字節(jié)內(nèi)存)。
- 分析對象內(nèi)存的時候不要忘記結(jié)構(gòu)體內(nèi)存對齊(結(jié)構(gòu)體的大小必須是最大成員大小的倍數(shù)碘箍,一般是8)和iOS內(nèi)存對齊(對象內(nèi)存大小必須是16的倍數(shù))遵馆。
- 兩個獲取內(nèi)存大小的函數(shù)
① size_t class_getInstanceSize(Class _Nullable cls)
獲取實例對象的成員變量所占用內(nèi)存大小(內(nèi)存對齊后的)-> 其實就是實例對象至少占用的內(nèi)存大小丰榴。
sizeof同上团搞,返回的是傳入類型至少占用的內(nèi)存大小,sizeof是個運(yùn)算符多艇。
② size_t malloc_size(const void *ptr)
獲取指針?biāo)赶騼?nèi)存的大小 -> 其實就是實例對象實際占用的內(nèi)存大小逻恐。
Demo地址:NSObject本質(zhì)