前言
Objective-C語言是一門高級語言指攒,底層是由C/C++語言實現(xiàn)蒿往。要想從本質(zhì)上了解Objective-C對象的底層數(shù)據(jù)結(jié)構(gòu)和內(nèi)存布局咽块,就需要一步步揭開那最神秘的面紗。
Objective-C對象經(jīng)過編譯鏈接運行后锭碳,所經(jīng)歷的過程如下所示:
在后面的講解中,主要將Objective-C對象一步步轉(zhuǎn)為最底層的實現(xiàn)勿璃。
將Objective-C語言轉(zhuǎn)換為C/C++語言
在終端執(zhí)行下面的命令擒抛,可以將Objective-C對象轉(zhuǎn)換成C/C++語言:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
舉例說明:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
執(zhí)行上述命令后,得到的結(jié)果如下:
struct NSObject_IMPL {
Class isa;
};
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;
}
通過上述編譯后的代碼可以看出补疑,NSObject對象的底層數(shù)據(jù)結(jié)構(gòu)是結(jié)構(gòu)體歧沪。
struct NSObject_IMPL {
Class isa;
};
如何獲取NSObject對象的內(nèi)存大小莲组?
獲取NSObject對象的內(nèi)存大小诊胞,需要用到以下幾個函數(shù):
- class_getInstanceSize
- malloc_size
- sizeOf
NSObject *obj = [[NSObject alloc] init];
NSLog(@"class_getInstanceSize = %zd", class_getInstanceSize([NSObject class]));
NSLog(@"malloc_size = %zd", malloc_size((__bridge const void *)(obj)));
NSLog(@"sizeOf = %zd", sizeof(obj));
控制臺打印如下:
class_getInstanceSize = 8
malloc_size = 16
sizeOf = 8
獲取結(jié)果居然不一樣,那是為什么呢锹杈?那就繼續(xù)探究一下源碼實現(xiàn)吧撵孤!
1、class_getInstanceSize
這個是一個runtime提供的API竭望,用于獲取類實例對象所占用的內(nèi)存大小邪码,返回所占用的字節(jié)數(shù)。
在蘋果開源網(wǎng)站咬清,找到對應的objc4-779.1.zip
壓縮包闭专∨耍看一下源碼實現(xiàn),在objc-class.mm
文件到找到了該方法的實現(xiàn)影钉,如下所示:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
Class's ivar size rounded up to a pointer-size boundary
翻譯一下画髓,返回實例對象中成員變量內(nèi)存大小。說白了平委,class_getInstanceSize就是獲取實例對象中成員變量內(nèi)存大小雀扶。
仔細想一下,實例對象在創(chuàng)建的時候肆汹,系統(tǒng)應該就會分配對應的內(nèi)存空間愚墓,那在對象初始化的過程中,是否有對應的內(nèi)存分配呢昂勉?
2浪册、alloc
我們都知道初始化一個OC對象是有兩個步驟:
- 給對象分配一個內(nèi)存空間
- 初始化該對象
當我們 alloc
的時候系統(tǒng)會分配內(nèi)存空間(地址)給OC對象,當 init
的時候?qū)崿F(xiàn)了對象的初始化工作岗照。就完成了一個對象的創(chuàng)建過程村象。
當執(zhí)行alloc的時候,系統(tǒng)會自動調(diào)用分配內(nèi)存地址的方法:
對象的創(chuàng)建離不開alloc
方法攒至,對象創(chuàng)建的過程中可能存在分配內(nèi)存空間的方法厚者,一起看下源碼。
在NSObject.mm
類中找到alloc
以及allocFromZone
方法的實現(xiàn):
+ (id)alloc {
return _objc_rootAlloc(self);
}
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
找到時機調(diào)用的核心方法是:_objc_rootAllocWithZone
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
繼續(xù)查找:_class_createInstanceFromZone
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes); //
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
在調(diào)用calloc
或者malloc_zone_calloc
函數(shù)是需要傳入size
參數(shù)迫吐,可以發(fā)現(xiàn)size變量來源于下面的代碼:
size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
CF requires all objects be at least 16 bytes.
CoreFoundation 框架要求所有對象至少分配16個字節(jié)库菲。
當實例對象不足16個字節(jié),系統(tǒng)分配給16個字節(jié)志膀,屬于系統(tǒng)的硬性規(guī)定熙宇。
仔細看,會發(fā)現(xiàn)alignedInstanceSize
函數(shù)不就是class_getInstanceSize
函數(shù)的內(nèi)部實現(xiàn)溉浙。
3烫止、malloc_size
這個函數(shù)主要獲取 系統(tǒng)實際分配的內(nèi)存大小,具體的底層實現(xiàn)也可以在源碼libmalloc找到
4戳稽、sizeOf
值得注意的一點是馆蠕,sizeof
是操作符,不是函數(shù)惊奇,它的作用對象是數(shù)據(jù)類型互躬,主要作用于編譯時。
因此赊时,它作用于變量時吨铸,也是對 其類型 進行操作。得到的結(jié)果是該數(shù)據(jù) 類型 占用空間大小祖秒,即size_t
類型诞吱。
5舟奠、應用
通過上面的學習,我們可以很好回答下面的這個經(jīng)典的問題了:
一個NSObject對象占用多少內(nèi)存房维?
在64位架構(gòu)下沼瘫, 系統(tǒng)分配了16個字節(jié)給NSObject對象(通過malloc_size函數(shù)獲得);
但NSObject對象內(nèi)部只使用了8個字節(jié)的空間(可以通過class_getInstanceSize函數(shù)獲得)咙俩。
內(nèi)存對齊
1耿戚、內(nèi)存對齊是什么?
內(nèi)存對齊 是一種在計算機內(nèi)存中 排列數(shù)據(jù)(表現(xiàn)為變量的地址)阿趁、訪問數(shù)據(jù)(表現(xiàn)為CPU讀取數(shù)據(jù))的一種方式膜蛔。
它包含了兩種相互獨立又相互關聯(lián)的部分:基本數(shù)據(jù)對齊 和 結(jié)構(gòu)體數(shù)據(jù)對齊 。
在iOS開發(fā)過程中脖阵,編譯器會自動的進行字節(jié)對齊的處理皂股,并且在64位架構(gòu)
下,是以8字節(jié)
進行內(nèi)存對齊的命黔。
2呜呐、內(nèi)存對齊的原則
內(nèi)存對齊應該是編譯器的管轄范圍,編譯器為程序中的每個數(shù)據(jù)單元安排在適當?shù)奈恢蒙虾纺迹奖阌嬎銠C快速高效的進行讀取數(shù)據(jù)蘑辑。
每個平臺的編譯器都有自己的對齊系數(shù)和相應的對齊規(guī)則。在iOS中的64位架構(gòu)下坠宴,對齊系數(shù)就是8個字節(jié)洋魂。
注意: 內(nèi)存對齊有實際占用的內(nèi)存對齊,也有系統(tǒng)分配內(nèi)存對齊啄踊,iOS中的64位架構(gòu)下忧设,系統(tǒng)分配對齊系數(shù)是16個字節(jié)荆萤,比如一個 NSObject 對象實際占用8個字節(jié)彩扔,但是系統(tǒng)分配16個字節(jié)
例如:代碼申請4個字節(jié)的空間猪瞬,但是因為內(nèi)存對齊,系統(tǒng)實際分配了16個字節(jié)
void *p = malloc(4);
NSLog(@"%zd", malloc_size(p));
[7436:1197123] 16
2.1 數(shù)據(jù)成員對齊
結(jié)構(gòu)體或者共用體中的成員變量中顿锰,首個成員變量放在偏移量為0的位置上,后面的成員變量的對齊偏移量是取指定對齊系數(shù)和本身該成員變量所占用大小中的較小值启搂,即 min(對齊系數(shù),成員變量的內(nèi)存大小 )
2.2 數(shù)據(jù)整體對齊
在結(jié)構(gòu)體或者共用體中的成員變量完成自身的對齊之后硼控,整個結(jié)構(gòu)體或者共用體也需要進行字節(jié)對齊處理,一般為 min(對齊系數(shù),最大成員變量的內(nèi)存大小 )的整數(shù)倍胳赌。
結(jié)合上述原則1牢撼、2,可以推斷出下面的常用原則疑苫,以結(jié)構(gòu)體為例:
- 結(jié)構(gòu)體變量的首地址是其最長基本類型成員的整數(shù)倍熏版;
- 結(jié)構(gòu)體每個成員相對于結(jié)構(gòu)體首地址的偏移量(offset)都是成員大小的整數(shù)倍纷责,如不滿足,對前一個成員填充字節(jié)以滿足撼短;
- 結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最大基本類型成員變量大小的整數(shù)倍再膳;
- 結(jié)構(gòu)體中的成員變量都是分配在連續(xù)的內(nèi)存空間中。
在熟悉上述對齊原則基礎上曲横,默認在64位架構(gòu)下喂柒,舉個例子:
struct object {
int a; // 4
NSString *b; // 8
int c; // 4
char d; // 1
};
結(jié)構(gòu)體中最大的成員變量占用8個字節(jié),根據(jù)上面的對齊原則禾嫉,最終獲得的對齊系數(shù)是min(最大成員變量大小8個字節(jié), 對齊系數(shù)8個字節(jié)) = 8灾杰。
不考慮內(nèi)存對齊的情況下,實際占用4 + 8 + 4 + 1 = 17個字節(jié)熙参,考慮字節(jié)對齊的情況下艳吠,分配24個字節(jié)。
例2:
struct object {
int a; // 4
char b; // 1
int c; // 4
};
根據(jù)上面結(jié)構(gòu)體尊惰,可以得出需要對齊的字節(jié)數(shù)為min(對齊系數(shù), 最大成員變量的內(nèi)存大小) = 4個字節(jié)讲竿。對齊后的內(nèi)存分配表如下所示:
3、內(nèi)存對齊的原因
為了減少CPU訪問內(nèi)存的次數(shù),提高計算機性能,一些計算機硬件平臺要求存儲在內(nèi)存中的變量按自然邊界對齊弄屡。
3.1 性能上的提升
3.2 跨平臺
4题禀、內(nèi)存對齊的注意事項
4.1 內(nèi)存分配
在結(jié)構(gòu)體中,聲明成員變量的順序不一致膀捷,也會導致最終分配內(nèi)存大小的不同迈嘹。
struct object {
int a; // 4
NSString *b; // 8
int c; // 4
};
對齊情況下,系統(tǒng)分配24個字節(jié)全庸,具體分配如下:
調(diào)換一下成員變量的聲明順序:
struct object {
int a; // 4
int c; // 4
NSString *b; // 8
};
這種情況下秀仲,系統(tǒng)分配16個字節(jié),具體分配如下:
通過上面對比可以看出壶笼,在日常開發(fā)中神僵,設計結(jié)構(gòu)的時候,合理調(diào)換成員變量的順序覆劈,可以很好地節(jié)省內(nèi)存空間保礼。
OC對象的內(nèi)存布局
情景一:帶有一個成員變量的對象占用內(nèi)存的大小
@interface Animal : NSObject
{
@public
int _age;
}
@end
Animal *animal = [[Animal alloc] init];
NSLog(@"Animal -- class_getInstanceSize = %zd", class_getInstanceSize([animal class]));
NSLog(@"Animal -- malloc_size = %zd", malloc_size((__bridge const void *)(animal)));
NSLog(@"Animal -- sizeOf = %zd", sizeof(animal));
2020-04-25 13:34:44.353148+0800 ClangDemo[7571:1234217] Animal -- class_getInstanceSize = 16
2020-04-25 13:34:44.353241+0800 ClangDemo[7571:1234217] Animal -- malloc_size = 16
2020-04-25 13:34:44.353264+0800 ClangDemo[7571:1234217] Animal -- sizeOf = 8
情景二:不同成員變量的對象占用內(nèi)存的大小
在情景一的基礎上,在Animal對象再添加一個成員變量_weight责语,如下所示:
@interface Animal : NSObject
{
@public
int _age;
int _weight;
}
@end
2020-04-25 13:42:41.791946+0800 ClangDemo[7575:1236418] Animal -- class_getInstanceSize = 16
2020-04-25 13:42:41.792034+0800 ClangDemo[7575:1236418] Animal -- malloc_size = 16
2020-04-25 13:42:41.792058+0800 ClangDemo[7575:1236418] Animal -- sizeOf = 8
情景三:繼續(xù)添加不同類型的成員變量
添加整型成員變量
@interface Animal : NSObject
{
@public
int _age;
int _weight;
int _height;
}
@end
2020-04-25 13:44:27.894359+0800 ClangDemo[7577:1237260] Animal -- class_getInstanceSize = 24
2020-04-25 13:44:27.894433+0800 ClangDemo[7577:1237260] Animal -- malloc_size = 32
2020-04-25 13:44:27.894453+0800 ClangDemo[7577:1237260] Animal -- sizeOf = 8
添加字符串型成員變量
NSString *_name;
2020-04-25 13:46:12.214979+0800 ClangDemo[7581:1238086] Animal -- class_getInstanceSize = 32
2020-04-25 13:46:12.215062+0800 ClangDemo[7581:1238086] Animal -- malloc_size = 32
2020-04-25 13:46:12.215085+0800 ClangDemo[7581:1238086] Animal -- sizeOf = 8
情景四:調(diào)換成員變量聲明順序
情況一:整型變量中摻雜字符串變量
int _age;
NSString *_name;
int _weight;
NSString *_nick;
int _height;
2020-04-25 13:47:56.280581+0800 ClangDemo[7584:1238907] Animal -- class_getInstanceSize = 48
2020-04-25 13:47:56.280654+0800 ClangDemo[7584:1238907] Animal -- malloc_size = 48
2020-04-25 13:47:56.280675+0800 ClangDemo[7584:1238907] Animal -- sizeOf = 8
情況二:調(diào)換一下聲明成員變量的順序
int _age;
int _weight;
int _height;
NSString *_name;
NSString *_nick;
2020-04-25 13:49:21.888761+0800 ClangDemo[7588:1239645] Animal -- class_getInstanceSize = 40
2020-04-25 13:49:21.888838+0800 ClangDemo[7588:1239645] Animal -- malloc_size = 48
2020-04-25 13:49:21.888860+0800 ClangDemo[7588:1239645] Animal -- sizeOf = 8
情景五:繼承體系下的內(nèi)存分配
@interface Animal : NSObject
{
@public
int _age;
int _weight;
}
@end
@interface Dog : Animal
{
@public
int _height;
}
2020-04-25 13:53:47.886732+0800 ClangDemo[7592:1240968] Dog -- class_getInstanceSize = 24
2020-04-25 13:53:47.886810+0800 ClangDemo[7592:1240968] Dog -- malloc_size = 32
2020-04-25 13:53:47.886831+0800 ClangDemo[7592:1240968] Dog -- sizeOf = 8
參考文章