一個NSObject對象占用多少內存?
Objective-C的本質
平時我們編寫的OC代碼,底層實現都是C/C++代碼
Objective-C --> C/C++ --> 匯編語言 --> 機器碼
所以Objective-C的面向對象都是基于C/C++的數據結構實現的,所以我們可以將Objective-C代碼轉換成C/C++代碼,來研究OC對象的本質。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
我們在main函數里面定義一個簡單對象,然后通過 clang -rewrite-objc main.m -o main.cpp
命令,將main.m
文件進行重寫,即可轉換出對應的C/C++代碼。但是可以看到一個問題,就是轉換出來的文件過長,將近10w行。
因為不同平臺支持的代碼不同(Windows/Mac/iOS),那么同樣一句OC代碼,經過編譯,轉成C/C++代碼,以及最終的匯編碼,是不一樣的,匯編指令嚴重依賴平臺環境。
我們當前關注iOS開發,所以,我們只需要生成iOS支持的C/C++代碼。因此,可以使用如下命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc <OC源文件> -o <輸出的cpp文件>
-sdk
:指定sdk
-arch
:指定機器cpu架構(模擬器-i386、32bit、64bit-arm64 )
如果需要鏈接其他框架,使用-framework參數,比如-framework UIKit
一般我們手機都已經普及arm64,所以這里的架構參數用arm64,生成的cpp代碼如下
接下來,我們查看一下main_arm64.cpp源文件,如果熟悉這個文件,你將會發現這么一個結構體
struct NSObject_IMPL {
Class isa;
};
我們再來對比看一下NSObject頭文件的定義
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end
簡化一下,就是
@interface NSObject {
Class isa ;
}
@end
是不是猜到點什么了?沒錯,struct NSObject_IMPL
其實就是NSObject的底層結構,或者說底層實現。換個角度理解,可以說C/C++的結構體類型支撐了OC的面相對象。
點進Class的定義,我們可以看到 是typedef struct objc_class *Class;
Class isa; 等價于 struct objc_class *isa;
所以NSObject對象內部就是放了一個名叫isa
的指針,指向了一個結構體 struct objc_class
。
總結一:一個OC對象在內存中是如何布局的?
猜想:NSObject對象的底層就是一個包含了一個指針的結構體,那么它的大小是不是就是8字節(64位下指針類型占8個字節)?
為了驗證猜想,我們需要借助runtime提供的一些工具,導入runtime頭文件,
class_getInstanceSize ()
方法可以計算一個類的實例對象所實際需要的的空間大小
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject對象的大?。?zd",size);
}
return 0;
}
結果是
完美驗證,it's over,let's go home!
等等,就這么簡單?確定嗎?答案是否定的~~~
介紹另一個庫#import <malloc/malloc.h>
,其下有個方法 malloc_size()
,該函數的參數是一個指針,可以計算所傳入指針 所指向內存空間的大小
。我們來用一下
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject實例對象的大?。?zd",size);
size_t size2 = malloc_size((__bridge const void *)(obj));
NSLog(@"對象obj所指向的的內存空間大?。?zd",size2);
}
return 0;
}
結果是16,如何解釋呢?想要真正弄清楚其中的緣由,就需要去蘋果官方的開源代碼里面去一探究竟了。蘋果的開源代請看這里。
先看一下class_getInstanceSize
的實現。我們需要進到objc4/文件里面下載一份最新的源碼,我當前最新的版本是objc4-750.1.tar.gz。下載解壓之后,打開工程,就可以查看runtime的實現源碼。
搜索class_getInstanceSize
找到實現代碼
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
再點進alignedInstanceSize
方法的實現
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
可以看到該方法的注釋說明Class's ivar size rounded up to a pointer-size boundary.
,意思就是獲得類的成員變量的大小,其實也就是計算類所對應的底層結構體的大小,注意后面的這個rounded up to a pointer-size boundary
指的是系統在為類的結構體分配內存時所進行的內存對齊,要以一個指針的長度作為對齊系數,64位系統指針長度(字長)是8個字節,那么返回的結果肯定是8的最小整數倍。為什么需要用指針長度作為對齊系數呢?因為類所對應的結構體,在頭部的肯定是一個isa
指針,所以指針肯定是該結構體中最大的基本數據類型,所以根據結構體的內存對齊規則,才做此設定。如果對這里有疑惑的話,請先復習一下有關內存對齊的知識,便一目了然了。
所以class_getInstanceSize
方法,可以幫我們獲取一個類的的實例對象所對應的結構體的實際大小。
我們再從alloc
方法探究一下,alloc
方法里面實際上是AllocWithZone
方法,我們在objc
源碼工程里面搜索一下,可以在Object.mm
文件里面找到一個_objc_rootAllocWithZone
方法。
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
再點進里面的關鍵方法class_createInstance
的實現看一下
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
繼續點進_class_createInstanceFromZone
方法
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
這個方法有點長,有時分析一個方法,不要過分拘泥細節,先針對我們尋找的問題,找到關鍵點,像這個比較長的方法,我們知道,它的主要功能就是創建一個實例,為其開辟內存空間,我們可以發現中間的這句代碼obj = (id)calloc(1, size);
,是在分配內存,這里的size
是需要分配的內存的大小,那這句應該就是為對象開辟內存的核心代碼,再看它里面的參數size
,我們能在上兩行代碼中找到size_t size = cls->instanceSize(extraBytes);
,于是我們繼續點進instanceSize
看看
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
翻譯一下這句注//CF requires all objects be at least 16 bytes.
我們就明白了,CF作出了硬性的規定:當創建一個實例對象的時候,為其分配的空間不能小于16
個字節,為什么這么規定呢,我個人目前的理解是這可能就相當于一種開發規范,或者對于CF框架內部的一些實現提供的規范。
這個size_t instanceSize(size_t extraBytes)
返回的字節數,其實就是為 為一個類創建實例對象所需要分配的內存空間。這里我們的NSObject
類創建一個實例對象,就分配了16個字節。
我們在點進上面代碼中的alignedInstanceSize
方法
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
這不就是我們上面分析class_getInstanceSize
方法里面看到的那個alignedInstanceSize
嘛。
總結二:class_getInstanceSize
&malloc_size
的區別
-
class_getInstanceSize
:獲取一個objc類的實例的實際大小,這個大小可以理解為創建這個實例對象至少需要的空間(系統實際為這個對象分配的空間可能會比這個大,這是出于系統內存對齊的原因)。 -
malloc_size
:得到一個指針所指向的內存空間的大小。我們的OC對象就是一個指針,利用這個函數,我們可以得到該對象所占用的內存大小,也就是系統為這個對象(指針)所指向對象所實際分配的內存大小。
sizeof()
:獲取一個類型或者變量所占用的存儲空間,這是一個運算符。 -
[NSObject alloc]
之后,系統為其分配了16個字節的內存,最終obj
對象(也就是struct NSObject_IMPL
結構體),實際使用了其中的8個字節內存,(也就是其內部的那個isa
指針所用的8個字節,這里我們是在64位系統為前提下來說的)
關于運算符和函數的一些對比理解
- 函數在編譯完之后,是可以在程序運行階段被調用的,有調用行為的發生
- 運算符則是在編譯按一刻,直接被替換成運算后的結果常量,跟宏定義有些類似,不存在調用的行為,所以效率非常高
更為復雜的自定義類
我們開發中會自定義各種各樣的類,基本上都是NSObject
的子類。更為復雜的子類對象的內存布局又是如何的呢?我們新建一個NSObject
的子類Student
,并為其增加一些成員變量
@interface Student : NSObject
{
@public
int _age;
int _no;
}
@end
@implementation Student
@end
使用我們之前介紹過的方法,查看一下這個類的底層實現代碼
struct NSObject_IMPL {
Class isa;
};
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _no;
};
我們發現其實Student
的底層結構里,包含了它的成員變量,還有一個NSObject_IMPL
結構體變量,也就是它的父類的結構體。根據我們上面的總結,NSObject_IMPL
結構體需要的空間是8字節,但是系統給NSObject
對象實際分配的內存是16字節,那么這里Student
的底層結構體里面的成員變量NSObject_IMPL
應該會得到多少的內存分配呢?我們驗證一下。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
//獲取`NSObject`類的實例對象的成員變量所占用的大小
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject實例對象的大?。?zd",size);
//獲取obj所指向的內存空間的大小
size_t size2 = malloc_size((__bridge const void *)(obj));
NSLog(@"對象obj所指向的的內存空間大?。?zd",size2);
Student * std = [[Student alloc]init];
size_t size3 = class_getInstanceSize([Student class]);
NSLog(@"Student實例對象的大小:%zd",size3);
size_t size4 = malloc_size((__bridge const void *)(std));
NSLog(@"對象std所指向的的內存空間大?。?zd",size4);
}
return 0;
}
從結果可以看出,
Student
類的底層結構體等同于
struct Student_IMPL {
Class isa;
int _age;
int _no;
};
總結一下就是,一個子類的底層結構體,相當于 其父類結構體里面的所有成員變量 + 該子類自身定義的成員變量 所組成的一個結構體。
出于嚴謹,我又給Student類多加了幾個成員變量,驗證我的猜想。
@interface Student : NSObject
{
@public
int _age;
int _no;
int _grade;
}
貌似是對的了,但是為什么用malloc_size
得到std
所被分配的內存是32?再來一發試試
@interface Student : NSObject
{
@public
//父類的isa還會占用8個字節
int _age;//4字節
int _no;//4字節
int _grade;//4字節
int *p1;//8字節
int *p2;//8字節
}
Student
結構體所有成員變量所需要的總空間為 36字節,根據內存對齊原則,最后結構體所需要的空間應該是8的倍數,那應該就是40,我們看一下結果
從結果看沒錯,但是同時也發現了一個規律,隨著
std
對象成員變量的增加,系統為Student
對象std
分配的內存空間總是以16的倍數增加(16~32~48......),我們之前分析源碼好像沒看到有做這個設定
其實上面這個方法只是可以用來計算一個結構體對象所實際需要的內存大小。[update]其實instanceSize()
-->alignedInstanceSize()
只是可以用來計算一個結構體對象理論上(按照內存對其規則)所需要分配的內存大小。
真正給實例對象完成分配內存操作的是下面這個方法calloc()
這個方法位于蘋果源碼的libmalloc文件夾中。但是里面的代碼再往下深究,介于我目前的知識儲備以及專業出身(數學專業),還是困難比較大。好在從一些大神那里得到了指點。
剛才文章開始,我們討論到了結構體的內存對齊,這是針對數據結構而言的。從系統層面來說,就以蘋果系統而言,出于對內存管理和訪問效率最優化的需要,會實現在內存中規劃出很多塊,這些塊有大有小,但都是16的倍數,比如有的是32,有的是48,在
libmalloc
源碼的nano_zone.h
里面有這么一段代碼
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */
NANO是源碼庫里面的其中一種內存分配方法,類似的還有frozen
、legacy
、magazine
、purgeable
。
這些是蘋果基于各種場景優化需求而設定的對應的內存管理相關的庫,暫時不用對其過分解讀。
上面的NANO_MAX_SIZE
解釋中有個詞Buckets sized
,就是蘋果事先規劃好的內存塊的大小要求,針對nano
,內存塊都被設定成16的倍數,并且最大值是256。舉個例子,如果一個對象結構體需要46個字節,那么系統會找一塊48字節的內存塊分配給它用,如果另一個結構體需要58個字節,那么系統會找一塊64字節的內存塊分配給它用。
到這里,應該就可以基本上解釋清楚,為什么剛才student
結構需要40個字節的時候,被分配到的內存大小確實48個字節。至此,針對一個NSObject
對象占用內存的問題,以及延伸出來的內存布局,以及其子類的占內存問題,應該就都可以得到解答了。
面試題解答
- 一個NSObject對象占用多少內存?
1)系統分配了16字節給NSObject對象(通過malloc_size
函數可以獲得)
2)NSObject對象內部只使用了8個字節的空間,用來存放isa
指針變量(64位系統下,可以通過class_getInstanceSize
函數獲得)