探尋OC對象的本質,咱們平時編寫的Objective-C代碼,底層實現其實都是C\C++代碼。bash
OC的對象都是經過基礎C\C++的結構體實現的。架構
咱們經過建立OC對象,並將OC文件轉化爲C++文件來探尋OC對象的本質,以下代碼:框架
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
}
return 0;
}
複製代碼
使用 clang 將 OC 代碼轉換爲 C++ 代碼iphone
// 這種方式沒有指定架構例如arm64架構 其中cpp表明(c plus plus)生成 main.cpp
clang -rewrite-objc main.m -o main.cpp
複製代碼
使用 Xcode 工具 xcrun 進行轉換函數
// 能夠指定 arm64 架構 若是須要連接其餘框架,使用-framework參數。好比-framework UIKit
// -o 輸出的 cpp 文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
複製代碼
NSObject 對象的 OC 定義以下:(咱們能夠在Xcode中點擊進去查看)工具
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
複製代碼
NSObject 對應的結構體是 NSObject_IMPL
結構體,NSObject_IMPL
定義以下:佈局
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
複製代碼
NSObject_IMPL
結構體只有一個成員 isa 指針,而指針在64位架構中佔8個字節。也就是說一個NSObjec對象所佔用的內存是8個字節。ui
NSObject *objc = [[NSObject alloc] init];
複製代碼
上述一段代碼中系統爲 NSObject 對象分配 8個字節的內存空間,用來存放一個成員 isa 指針。那麼 isa 指針這個變量的地址就是結構體的地址,也就是 NSObjcet 對象的地址。spa
NSObject 只須要8字節的空間,但實際上,NSObject 對象佔用 16 字節空間,iOS 下對象至少佔用 16 字節指針
代碼以下:
@interface Student : NSObject{
@public
int _no;
int _age;
}
@end
@implementation Student
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu -> _no = 4;
stu -> _age = 5;
NSLog(@"%@",stu);
}
return 0;
}
@end
複製代碼
對應的 C++ 結構體 Student_IMPL 以下:
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
複製代碼
所以此結構體佔用多少存儲空間,對象就佔用多少存儲空間。所以結構體佔用的存儲空間爲,isa指針8個字節空間,int類型 _no 佔用4個字節空間,int類型 _age 佔用4個字節空間,共16個字節空間。
image.png
sutdent對象的3個變量分別有本身的地址。而stu指向isa指針的地址。所以stu的地址爲0x100400110,stu對象在內存中佔用16個字節的空間。而且通過賦值,_no裏面存儲4 ,_age裏面存儲5。
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 強制轉化
Student *stu = [[Student alloc] init];
stu -> _no = 4;
stu -> _age = 5;
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"_no = %d, _age = %d", stuImpl->_no, stuImpl->_age); // 打印出 _no = 4, _age = 5
}
return 0;
}
@end
複製代碼
上述代碼將oc對象強轉成Student_IMPL的結構體。也就是說把一個指向oc對象的指針,指向這種結構體。最終能夠轉化成功,因此對象在內存中的佈局與結構體在內存中的佈局相同。由此說明stu這個對象指向的內存確實是一個結構體。
/// Person
@interface Person : NSObject
{
int _age;
}
@end
/// Student
@interface Student : Person
{
int _no;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%zd %zd",
class_getInstanceSize([Person class]),
class_getInstanceSize([Student class])
);
}
return 0;
}
複製代碼
咱們發現只要是繼承自NSObject
的對象,那麼底層結構體內必定有一個isa指針。那麼他們所佔的內存空間是多少呢?單純的將指針和成員變量所佔的內存相加便可嗎?上述代碼實際打印的內容是16 16,也就是說,person對象和student對象所佔用的內存空間都爲16個字節。 其實實際上person對象確實只使用了12個字節。可是由於內存對齊的緣由。使person對象也佔用16個字節。
編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本數據類型,而後尋找內存地址能是該基本數據類型的整倍的位置,做爲結構體的首地址。將這個最寬的基本數據類型的大小做爲對齊模數。
爲結構體的一個成員開闢空間以前,編譯器首先檢查預開闢空間的首地址相對於結構體首地址的偏移是不是本成員的整數倍,如果,則存放本成員,反之,則在本成員和上一個成員之間填充必定的字節,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個字節。
複製代碼
咱們能夠總結內存對齊爲兩個原則:
原則 1. 前面的地址必須是後面的地址正數倍,不是就補齊。
原則 2. 整個Struct的地址必須是最大字節的整數倍。
原則 3. OC 對象至少佔 16 字節
複製代碼
經過上述內存對齊的原則咱們來看,person對象的第一個地址要存放isa指針須要8個字節,第二個地址要存放_age成員變量須要4個字節,根據原則一,8是4的整數倍,符合原則一,不須要補齊。而後檢查原則2,目前person對象共佔據12個字節的內存,不是最大字節數8個字節的整數倍,因此須要補齊4個字節,所以person對象就佔用16個字節空間。
而對於student對象,咱們知道sutdent對象中,包含person對象的結構體實現,和一個int類型的_no成員變量,一樣isa指針8個字節,_age成員變量4個字節,_no成員變量4個字節,恰好知足原則1和原則2,因此student對象佔據的內存空間也是16個字節。
建立一個實例對象,至少須要多少內存?
#import <objc/runtime.h> class_getInstanceSize([NSObject class]);
●
●
●
建立一個實例對象,實際上分配了多少內存?
#import <malloc/malloc.h> malloc_size((__bridge const void *)obj);
備註:sizeof()不是一個函數
複製代碼
因爲一些系統內存分配機制,內存對齊等,實際分配的內存可能要比實際須要的內存大一些。