*Q1:一個NSObject實例對象占用多少內存?*
NSObject 對于每一個iOS開發者來說都很熟悉,因為我們幾乎每時每刻都跟其打交道,但是我們可能不知道究竟這個熟悉的實例對象究竟占用我們內存的空間是多少呢?那么下面我們就一起來探討一下。
NSObject *obj = [[NSObject alloc] init];
上面這段代碼,相信大家很熟悉,就是alloc分配內存給對象,調用init方法為父類屬性進行初始化,然后用指針obj指向我們剛才分配的地址。一個NSObject實例對象占用多少內存,其實就是在問這個obj指針指向的內存空間占用的內存空間是有多大?
要想解決這個問題:
①我們需要理解我們平常編寫的OC代碼其實就是基于C、C++為基礎而"面向對象"的一門語言。
②其次需要知道的是NSObject在內存中究竟是如何布局的?
下面我們先來看看第一個問題解釋:
我們日常編寫的OC代碼最終會在編譯器的作用下轉成C、C++語言、再到匯編語言、機器語言。通過下面的兩張圖可以發現OC代碼編寫的Student類和用C、C++代碼寫的strut結構體,有異曲同工之妙,那么我們是不是可以認為OC中的類底層代碼其實就是C、C++的struct(結構體)?
確實是如此,那我們有沒有方法去證明呢?答案是有的,我們可以通過新建一個Mac OS的命令行工程,在main函數中輸入:
NSObject *obj = [[NSObject alloc] init];
然后打開我們的終端程序,輸入
clang -rewrite-objc main.m -o main.cpp 或者
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
我們通過使用xcode自帶的clang工具把OC代碼轉成C、C++代碼,那上面的兩個命令有什么不一樣的?我們都知道OC這門語言不單單只能開發iOS上面的應用程序,而且能夠開發MacOS、WatchOS等平臺上的一些程序,而第一條命令會把OC代碼轉換成支持所有平臺的C++程序,而第二條命令只會轉換成arm64架構的c++程序。之所以編譯成arm64架構支持的,是因為如今市面上的iPhone設備都使用該架構。
第一條命令和第二條命令生成的C++代碼我們把其拉到最底部,能夠發現我們main函數的代碼,然后通過搜索NSObject_IMPL
找到 stuct NSObject_IMPL
結構體。如下圖:
然后我們直接回到剛才編寫的OC命令行程序,按住Command鍵點入NSObject類,我們能夠看到我們NSObject其實就是下面這段代碼:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
再做一個簡單的去除無用的代碼即可得到:
@interface NSObject <NSObject> {
Class isa;
}
通過對比struct NSObject_IMPL
和 NSObject內部OC代碼
,我們可以證明OC中的類底層代碼其實就是C、C++的struct(結構體)。那回到我們一開始的問題:一個NSObject實例對象占用多少內存?我們可以發現struct NSObject_IMPL
中的isa
成員變量其實是typedef struct objc_class *Class;
這個種類型的指針。而我們知道一個類的地址是由他第一個成員變量的地址所決定的,也就是說如果isa
在內存中的地址為 0x100400110
那么這個NSObject實例對象
的地址值就是0x100400110
,因此我們能夠得出結論指向一個NSObject
實例對象的指針obj
的地址就是0x100400110
我們又知道一個指針在64位系統中占用的內存空間就是8個字節,那么我們可能會覺得只有一個isa
指針的NSObject實例對象
,它所占用的內存空間就是8個字節,其實這是不對的,其實一個NSObject實例對象
占用的內存空間為16個字節,為什么呢?我們不妨通過兩個函數來驗證一下。通過使用<objc/runtime.h>
中的class_getInstanceSize
方法和<malloc/malloc.h>
中的malloc_size
方法來打印一下NSObject實例對象
所占用的內存空間。
NSLog(@"class_getInstanceSize: %zd",class_getInstanceSize([NSObject class]));
NSLog(@"malloc_size: %zd",malloc_size((__bridge const void *)(obj)));
打印出來的分別是8和16,那為什么我們剛才說的是16個字節而不是8個字節呢?而且這兩個方法有什么區別呢?為什么是以malloc_size為標準呢?其實<objc/runtime.h>
中的class_getInstanceSize
方法所得到的是objc對象實際需要的內存大小,而<malloc/malloc.h>
中的malloc_size
方法所得到的是objc對象實際分配的內存大小。那為什么一個NSObject對象明明只需要8個字節的內存大小就可以了,但是還是分配到了16個字節大小的內存空間?對于這個問題我們可以通過閱讀objc4源碼來得到答案,地址https://opensource.apple.com/tarballs/ 。下載最新版本的objc4源碼。
通過跟蹤obj4中alloc和allocWithZone兩個函數的實現,會發現這個連個函數都會調用一個instanceSize的函數:
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16bytes.
if (size < 16) size = 16;
return size;
}
這個函數的代碼很簡單,返回的結果就是系統給一個對象分配內存的大小。當對象的實際大小小于16時,系統就返回16個字節的大小。也就是說16個字節大小是系統的最低消費。還是用坐車的例子來說明一下,假如有8個人想坐車,他們打電話叫車說要一輛能坐8個人大小的車,對方說sorry我們沒有坐8個人大小的車,我們這里最小的就是坐16個人的車。最后來了一輛坐16個人的車,拉了8個人開走了。車就好比一個NSOject對象,車上的乘客就好比是對象中的成員,車的大小或者說載客數量就相當于一個對象占用的內存大小,車上實際的乘客數量就是對象中成員的大小。所以說一個NSObject對象占用多少內存,我想應該很明白了。
總結:
系統分配了16個字節給NSObject 對象(通過malloc_size獲得)
但NSObject對象內部只使用了8個字節(64bit 通過class_getInstanceSize)
另外:
我們可以通過view memory、lldb的指令去驗證我們的結論,這些操作會再后面制作的視頻講解中附上。
下一節:
會繼續深入,推算針對我們自定義的類內存布局和對象占用的內存空間。