NSObject 底層本質
-
一、OC 轉 C/C++
-
二、NSObject 物件記憶體佈局
-
三、NSObject 記憶體大小
-
四、OC 物件記憶體佈局
-
五、OC 物件記憶體大小
一、OC 轉 C/C++
OC 的底層是通過 C\C++ 實現,所以 OC 程式碼編譯過程一般是先將 OC 轉為 C\C++ ,C\C++ 進一步轉為組合語言,最終轉為機器程式碼。OC 的物件對映到 C\C++ 主要對應的是結構體,這裡面的 “結構體” 並非 C 語言裡面的結構體,而是 C++ 語言裡面的結構體,而且這個概念僅限字面意思的結構體。嚴格來講,其實struct關鍵字定義的是 類,跟 class 關鍵字定義的類除了預設訪問許可權的區別,沒有區別。C++ 中的 struct 對 C 中的 struct 進行了擴充,它已經不再只是一個包含不同資料型別的資料結構了,它已經獲取了太多的功能。如:能包含成員函式、可以繼承、可以實現多型。
通過 xcrun 命令可以將 OC 程式碼轉為不同平臺CPU下支援的 C\C++ 程式碼,如 OC 程式碼轉為 arm64 架構 CPU 程式碼,對應的命令為:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC原始檔 -o 輸出的CPP檔案
二、NSObject 物件本質
int main(int argc, const char * argv[]) { @autoreleasepool { NSObject *obj = [[NSObject alloc] init]; } return 0; }
點選可檢視NSObject定義為如下,可以看出 NSObject 類中包含了一個 isa 成員變數。
@interface NSObject <nsobject> { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-interface-ivars" Class isaOBJC_ISA_AVAILABILITY; #pragma clang diagnostic pop } </nsobject>
上述程式碼藉助 xcrun 命令生成的檔案中包含如下程式碼,實際上NSObject的定義最終也是轉為如下程式碼。
//其中 Class 的定義為:typedef struct objc_class *Class; 64位系統中,指標佔據 8 個位元組 struct NSObject_IMPL { Class isa; // 8個位元組 };
NSObject *obj = [[NSObject alloc] init];的記憶體佈局如下。alloc相當於為為右側藍色的結構體開闢一塊空間,結構體中儲存著 isa 成員,isa 成員的指標的地址相當於結構體地址空間,初始化成功後,結構體的地址賦值給 obj 物件,因此 isa 地址和 obj 地址相同。
三、物件記憶體大小
3.1 檢視記憶體管大小
//#import <objc runtime="" h=""> NSObject *obj = [[NSObject alloc] init]; // 獲得NSObject例項物件的成員變數所佔用的大小 >> 8 NSLog(@"%zd", class_getInstanceSize([NSObject class])); // 獲得obj指標所指向記憶體的大小 >> 16 NSLog(@"%zd", malloc_size((__bridge const void *)obj)); </objc>
class_getInstanceSize方法可以獲取例項物件的成員變數大小,即建立一個例項物件,至少需要多少記憶體。malloc_size方法可以獲取物件指標所指向記憶體大小,即建立一個例項物件,實際上分配了多少記憶體。兩個方法的區別具體可以看runtime底層原始碼觀察其區別。OC 底層原始碼一般可在該網站檢視。
3.2 class_getInstanceSize 函式
class_getInstanceSize底層實現如下,其中英文註釋很清晰的描述了該方法返回的是成員變數(Class's ivar)大小。
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() { return word_align(unalignedInstanceSize()); }
3.3 alloc 函式
OC 中的alloc在底層呼叫 runtime 的allocWithZone方法:
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; }
id class_createInstance(Class cls, size_t extraBytes) { return _class_createInstanceFromZone(cls, extraBytes, nil); }
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 { //該處呼叫了C語言的 calloc 函式開闢空間,所以檢視NSObject 物件的建立開闢空間大小應當依次為切入點,檢視size 引數來源 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; }
上述程式碼呼叫了C語言的 calloc函式開闢空間,所以檢視NSObject 物件的建立開闢空間大小應當依次為切入點,檢視size引數來源。alignedInstanceSize()方法是 class_getInstanceSize 方法底層來源,所以下面程式碼中的extraBytes變數值實際為 0。非常值得注意的是,下述程式碼中明確指出CF requires all objects be at least 16 bytes.,即 CF 物件至少為 16 位大小。
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; }
3.4 小結
綜上,系統分配了 16 個位元組給 NSObject 物件(通過 malloc_size 函式獲得),但 NSObject 物件內部只使用了 8 個位元組的空間,這8個位元組主要用來存放 isa( 64bit 環境下,可以通過 class_getInstanceSize 函式獲得)。
四、OC 物件記憶體佈局
4.1 簡單物件
@interface Student : NSObject { @public int _no; int _age; } @end @implementation Student @end int main(int argc, const char * argv[]) { @autoreleasepool { Student *stu = [[Student alloc] init]; } return 0; }
利用 xcrun 命名生成 C/C++ 程式碼,程式碼中會包含如下部分:
struct Student_IMPL { Class NSObject_IMP NSObject_IVARS; int _no; int _age; };
其中NSObject_IMP 定義為:
struct NSObject_IMPL { Class isa; };
Student_IMP底層進而可轉為如下結構,不難看出如果物件之間存在繼承關係,最終子類轉化對應的結構體會包含父類的結構體成分,且父類結構體成份在前。
struct Student_IMPL { Class isa; int _no; int _age; };
為了進一步說明OC物件底層是結構體實現的,可以將OC物件強制轉為結構體,發現程式碼依然正常執行。
Student *stu = [[Student alloc] init]; stu->_no = 4; stu->_age = 5; //使用__bridge 將 OC 轉為 C struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu; NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
結合上述程式碼,可以總結出Student物件的底層實現結構圖:
記憶體佈局大概是這樣,stu 指標指向結構體地址,結構體地址即為首個成員變數的地址。
4.2 繼承物件
如果是 Person 繼承自 NSObject ,Student 繼承自 Person 類,Person 中包含 age 成員變數, Student 包含 no 成員變數,則底層實現結構如下圖。
表面上看 Person_IMPL 佔據 8 + 4 = 12 個位元組,但是 OC 中明確指出一個 NSObject 物件大小至少為 16 位元組。從記憶體對齊角度分析來看,Person_IMPL結構體也至少為 16 位元組,即結構體的大小必須是最大成員的倍數,即8 * 2 = 16。
表面上看 Student_IMPL 佔據 16 + 4 = 20 個位元組實際並非如此,應該為 16 位元組。因為 Student_IMPL 中雖然包含Person_IMPL,但是在繼承關係中,Person_IMPL中 age 對應的記憶體空間並未被使用,所以應該是 8 + 4 + 4 = 16 位元組。
五、OC 物件記憶體大小
@interface MJPerson : NSObject { int _age; int _height; int _no; } @end MJPerson *p = [[MJPerson alloc] init]; NSLog(@"%zd%zd",class_getInstanceSize([MJPerson class]), malloc_size((__bridge const void *)(p))); // 24 32
p 物件對應的結構體為:
struct MJPerson_IMPL { struct NSObject_IMPL NSObject_IVARS; int _age; int _height; int _no; }; // 計算結構體大小,記憶體對齊,24
上述程式碼列印結果分別為 24 和 32,按照前面所說的結構體記憶體對齊原則來說,列印結果應該都為 24,即 8 的最小倍數, 8 * 3 = 24。class_getInstanceSize為結構體記憶體大小 8 + 4 + 4 + 4 = 24,但和實際情況確有一些出入。
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; }
size_t class_getInstanceSize(Class cls) { if (!cls) return 0; return cls->alignedInstanceSize(); }
結合 3.3 小結的原始碼來分析來看,class_getInstanceSize和alloc方法最終本質都是呼叫了alignedInstanceSize函式,其中alloc呼叫alignedInstanceSize函式中間使用到了 C 語言的 calloc函式,並且還涉及到一個變數extraBytes,但是此變數的值一般為 0,所以綜合來看 malloc_size((__bridge const void *)(p))的值為 32 和calloc密不可分。因為在calloc函式中存在記憶體對齊一說(注意此記憶體對齊不等同於結構體記憶體對齊),即在堆區每次分配記憶體為 16 的倍數。如果比較感興趣,可進一步檢視 calloc函式原始碼 ofollow,noindex">libmalloc 。
作者:ZhengYaWei
連結:https://www.jianshu.com/p/49947a53c4ed