本文屬筆記性質,主要針對自己理解不太透徹的地方進行記錄。
推薦系統直接學習小碼哥iOS底層原理班—MJ老師的課確實不錯,強推一波。
OC物件本質
基於C與C++結構體實現
OC語言如何被編譯器編譯:
OC ==>
C++ ==>
彙編 ==>
機器語言
而在C++中只有struct(結構體)
才能容納不同型別的內容(比如不同屬性
)。
將Objective-C程式碼轉換為C\C++程式碼
clang -rewrite-objc OC原始檔 -o 輸出的CPP檔案
將原始檔轉寫成通用的cpp檔案
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC原始檔 -o 輸出的CPP檔案
通過Xcode將原始檔轉寫成arm64架構下的iphoneos檔案,檔案內容比第一種要少
- 如果需要連結其他框架,使用-framework引數。比如-framework UIKit
NSObject的OC與C++定義
- 在OC中的定義
@interface NSObject <
NSObject>
{
Class isa;
}複製程式碼
- 轉成C++之後的定義
struct NSObject_IMPL {
Class isa;
};
複製程式碼
對於結構體來說,和陣列一樣。其第一個成員的地址,即為結構體物件的地址。所以一個OC物件的地址,實際上就是其isa指標的地址。
而這個isa
是指向objc_class
結構體的指標
// 指標typedef struct objc_class *Class;
複製程式碼
而一個指標在64位系統中所佔的記憶體為8位元組
所以一個OC物件所佔的記憶體至少為8位元組
NSObject物件所佔用記憶體的大小
上面的結論通過class_getInstanceSize
函式也可以佐證:
#import <
objc/runtime.h>
/*獲得NSObject例項物件的`成員變數`所佔用的大小 >
>
8*/NSLog(@"%zd", class_getInstanceSize([NSObject class]));
//runtime原始碼中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());
}複製程式碼
需要注意這個word_align
返回的是記憶體對齊後的大小,以unalignedInstanceSize
(為對齊的)大小作為引數。
而對於NSObject *obj
指標,我們有另一個函式可以檢視其實際被分配的記憶體大小
#import <
malloc/malloc.h>
// 獲得obj指標所指向記憶體的大小 >
>
16NSLog(@"%zd", malloc_size((__bridge const void *)obj));
複製程式碼
為什麼8位元組的結構體會被分配16位元組
繼續看runtime
+ (id)alloc {
return _objc_rootAlloc(self);
}id _objc_rootAlloc(Class cls){
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}static ALWAYS_INLINE idcallAlloc(Class cls, bool checkNil, bool allocWithZone=false){
if (slowpath(checkNil &
&
!cls)) return nil;
#if __OBJC2__ if (fastpath(!cls->
ISA()->
hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator. // fixme store hasCustomAWZ in the non-meta class and // add it to canAllocFast's summary if (fastpath(cls->
canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal. bool dtor = cls->
hasCxxDtor();
id obj = (id)calloc(1, cls->
bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->
initInstanceIsa(cls, dtor);
return obj;
} else {
// Has ctor or raw isa or something. Use the slower path. id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}#endif // No shortcuts available. if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}// Replaced by ObjectAlloc+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}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 {
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;
}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;
}複製程式碼
alloc函式最終會根據
instanceSize
返回的size
,然後使用calloc(1, size);
函式去分配記憶體。在
instanceSize
函式中,alignedInstanceSize
方法為成員變數所佔記憶體大小(上面已經貼過一次).extraBytes
引數(據我所見)都為0。而
CoreFoundation
框架在instanceSize
函式中硬性規定不足16位元組的記憶體地址會被補成16位位元組。但實際上,
NSObject
物件只使用了8位元組
用來儲存isa
指標
Student物件的本質
@interface Student : NSObject{
@public int _no;
int _age;
}@end複製程式碼
重寫成C++之後
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
struct NSObject_IMPL {
Class isa;
};
//其實就是struct Student_IMPL {
Class isa;
//8位元組 int _no;
//4位元組 int _age;
//4位元組
};
複製程式碼
所以一個
OC物件的本質
實際上是一個包含了所有父類成員變數
+自身成員變數
的結構體
Student的記憶體佈局及大小
可以通過Debug->
Debug workflow->
View momory檢視指定地址的結構來查證
對於Student例項物件所佔記憶體地址的大小,我們同樣可以通過malloc_size
函式來確定。
結果是16。8位元組父類的isa指標、4位元組_age的int、4位元組_no的int。
當然如果有興趣可以用memory write (stu地址+8偏移量) 8
的方式,通過直接修改記憶體的方式對成員變數_no
的值進行修改。
記憶體對齊原則下的OC物件記憶體分配
alignedInstanceSize()函式的記憶體對齊
alignedInstanceSize()
函式會按照所有成員變數中記憶體最長的一個做記憶體對齊。比如
@interface Animal: NSObject{
int weight;
int height;
int age;
}複製程式碼
實際上只需要8+4+4+4=20
個位元組長度即可,但是記憶體對其之後會返回8*3=24
malloc()/calloc()函式的記憶體對齊
在物件實際建立時,先以alignedInstanceSize()
返回的大小作為參考。然後calloc
在實際分配記憶體時為了記憶體對齊,最終將會根據bucket
進行分配。這個bucket
是16的整數倍。
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...
} */複製程式碼
所以Animal
的例項物件實際上會被分配32個位元組長度的記憶體地址。
sizeOf 與 class_getInstanceSize
返回一個引數物件所佔的記憶體大小
sizeOf
sizeOf
是運算子,在程式編譯階段將會直接替換成引數型別所佔記憶體具體的常數值。
由於在編譯階段替換,所以有以下這種特性:
MJPerson *p = [[MJPerson alloc] init];
NSLog(@"%zd", sizeof(p));
// 8複製程式碼
p
在編譯時將會被認為成指標,返回8位元組的指標記憶體長度。而不是MJPerson
型別的記憶體長度。
class_getInstanceSize
class_getInstanceSize
是一個方法,在程式執行階段將會進行計算。
他可以在執行階段計算某個類所需記憶體大小
class_getInstanceSize([p class]) //24複製程式碼
objc_class
runtime.h
OC2.0以前的類結構體。在2.0之後只剩下標頭檔案,並且已經標記成了
OBJC2_UNAVAILABLE
的棄用狀態。
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
複製程式碼
objc_runtime_new.h
最新的runtime原始碼中,優化了類的結構,內部分工更加明確。
在一級結構體中,只保留了
isa
、superclass
、cache
三個常用的成員其餘資訊均轉移到了
class_data_bits_t
這個二級結構體上
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
// 方法快取 class_data_bits_t bits;
// 具體的類資訊 class_rw_t *data() {
return bits.data();
} void setData(class_rw_t *newData) {
bits.setData(newData);
} ...
}複製程式碼
在
class_data_bits_t
(類資訊列表)內部,還儲存著class_rw_t
(可讀寫資訊列表),這些資訊是可以動態修改的
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure. uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
//方法 property_array_t properties;
//屬性 protocol_array_t protocols;
//協議 Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}複製程式碼
在
class_rw_t
(可讀寫資訊列表)內部,還儲存著class_ro_t
(不可變資訊列表),儲存著類載入進記憶體時就需要確定的資訊
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
//例項物件所佔記憶體大小#ifdef __LP64__ uint32_t reserved;
#endif const uint8_t * ivarLayout;
const char * name;
//類名 method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
//成員變數 const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
複製程式碼