本次講解的很多內容都涉及到objc的源碼,有興趣的可以去下載最新版本的objc4源碼。
1. OC對象的內存布局
1.1 一個NSObject對象占多少內存?
我們平時開發中說用到了絕大多數的類都是以NSObject
作為基類。我們進入NSObject.h
文件可以看到NSObject
類的定義如下:
@interface NSObject <NSObject> {
Class isa ;
}
我們將OC代碼轉成c/c++代碼后可以看到,NSObject
類是通過結構體
來實現的,如下所示:
struct NSObject_IMPL {
Class isa;
};
// Class的定義
typedef struct object_class *Class;
從上面可以看出這個結構體
和OC中NSObject
類的定義是一致的。這個類中只包含一個Class
類型的屬性,而Class是一個指向object_class
結構體的指針(結構體的地址就是結構體中第一個成員的地址
),所以isa
就是一個指針,占8個字節(64位機器上面)。那么,是不是意味著一個NSObject對象在內存中就占8個字節呢?我們通過代碼測試一下:
NSObject *obj = [[NSObject alloc] init];
// class_getInstanceSize()函數需要引入頭文件#import <objc/runtime.h>
NSLog(@"---%zd",class_getInstanceSize([obj class]));
// malloc_size()函數需要引入頭文件#import <malloc/malloc.h>
NSLog(@"---%zd",malloc_size((__bridge const void *)(obj)));
// ***************打印結果***************
2020-01-03 11:48:21.302884+0800 AppTest[62149:5950838] ---8
2020-01-03 11:48:21.303065+0800 AppTest[62149:5950838] ---16
-
class_getInstanceSize()
函數得到的結果和我們預期是一致的,這個函數是runtime
的一個函數,它返回的是類的一個實例的大小。我們查看objc4
源碼可以看到這個函數返回的是類的成員變量所占內存的大小(是內存對齊后的大小,結構體內存對齊的規則是結構體總大小必須是結構體中最大成員所占內存大小的倍數),所以得到的結果是8. -
malloc_size()
函數返回的是系統實際分配的內存大小,是16個字節,但是實際使用的只有8個字節。
所以,一個NSObject對象所占用的內存是16個字節
。為什么會分配16個字節呢?我們可以去objc4源碼
看看alloc
方法(在NSObject.mm
文件中)的調用流程:
alloc
-->_objc_rootAlloc()
-->callAlloc()
-->class_createInstance()
-->_class_createInstanceFromZone()
。
在_class_createInstanceFromZone()
函數中調用了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;
}
可以看到給一個對象分配內存的大小最小為16(這是系統硬性規定)。另外要注意的是一個實例對象占用多少內存和類中是否有方法是沒有關系的,因為類中的方法并不存放在實例對象中。
1.2 自定義對象占多少內存?
我們先定義一個繼承自NSObject
的Student
類,Student
類聲明了2個int類型屬性:
@interface Student : NSObject
@property (nonatomic , assign) int age;
@property (nonatomic , assign) int height;
@end
我們將其轉換為c/c++
代碼查看其底層結構:
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int age;
int height;
};
struct NSObject_IMPL {
Class isa;
};
Student_IMPL
結構體的第一個成員就是其父類的結構體,從上面我們可以得到2個信息:子類的成員變量包含了父類的成員變量;父類成員變量放在子類成員變量的前面。所以Student
類有3個成員變量:isa(8字節)、age(4字節)和height(4字節)。那一個Student
的實例對象是不是占16個字節呢?下面我們測試一下:
Student *stu = [[Student alloc] init];
NSLog(@"%zd",malloc_size((__bridge const void *)(stu)));
// ***************打印結果***************
2020-01-03 17:36:47.773438+0800 CommandLine[62909:6064600] 16
如果此時我們在Student
類中新增一個int類型的屬性weight,那一個Student
的實例對象是不是就占20個字節呢?測試發現結果并不是20,而是32
。為什么會這樣呢,這就涉及到了iOS的內存字節對齊,其結果就是系統給一個對象分配的內存大小都是16的倍數。所以系統給一個自定義類的實例對象分配內存時,先計算類的所有成員變量(包括父類以及整個繼承鏈的成員變量)的大小size,如果size剛好是16的倍數,那分配的內存的大小就是size;如果size不是16的倍數,那就將size補齊到剛好是16的倍數為止,補齊后的結果就是實際分配的內存大小。(結構體內存對齊字節數是8,OC對象內存對齊字節數是16,有關iOS系統分配內存時的對齊規則可以查看libmalloc
庫中的malloc.c
文件中的malloc_zone_calloc()
函數)。
為什么要進行內存對齊
呢?簡單來說就是未對齊的數據會大大降低CPU的性能。因為CPU讀取數據時不是一個字節一個字節進行讀取的,而是每次讀取一塊數據,塊的大小在不同的系統上是不一樣的,可以是2、4、8、16個字節。比如說如果CPU一次讀取16個字節,如果一個對象占用內存的大小不是16的倍數,那么CPU讀取這個對象數據時就需要做一些額外的操作,影響CPU的性能。
2. OC對象的分類
前面提到的對象都是實例對象,OC中除了實例對象
之外,還有另外兩種對象:類對象
和元類對象
。
2.1 實例對象
實例對象就是通過類alloc出來的對象,比如Student *student = [[Student alloc] init];
,這樣就創建了Student類的一個實例對象,每次調用alloc都會產生新的實例對象。
一個實例對象在內存中存儲的信息前面也提到了,它的內存結構是比較簡單的,就只存了實例對象的所有成員變量的數據:
- isa指針(isa指針也是成員變量,只是它比較特殊,所有OC對象都有isa指針,具體的后面會介紹)。
- 其他成員變量
2.2 類對象
OC中每個類都有一個與之對應的類對象,而且有且只有一個類對象
。與實例對象相比,類對象的內存結構要復雜很多(關于類的底層數據結構后面再做介紹),其在內存中存儲的信息主要包括:
- isa指針
- superclass指針
- 屬性信息
- 對象方法(
-
開頭的方法)信息 - 協議信息
- 成員變量信息(這里存儲的是成員變量名字、類型等信息,這個和實例對象中存儲的成員變量數據不是一個概念)。
- 其它一些信息。
獲取類對象的方法有多種,不管哪一種方法獲取到的類對象都是一樣的。
Student *stu = [[Student alloc] init];
// 1. 調用實例對象的class方法來獲取
Class stuClass1 = [stu class];
// 2. 調用類的class方法來獲取
Class stuClass2 = [Student class];
// 3. 調用runtime的object_getClass(object1);
Class stuClass3 = object_getClass(stu);
注意第3種方法不要寫錯了,runtime中還有另外一個很相似的函數:
// 上面用的是這個函數,傳入一個,返回這個對象所屬的類
Class object_getClass(id _Nullable obj);
// 這個方法是傳入一個字符串,返回類名是這個字符串的類
Class objc_getClass(const char * _Nonnull name);
2.3 元類對象
從上面介紹我們可以看出,類對象是用來存儲實例對象的信息的(比如實例方法、屬性等信息),那類對象的信息(比如類的類方法信息)又是存在哪里呢?這就是我們要介紹的元類對象
。
每個類在內存中有且只有一個元類對象
,元類對象和類對象的內存結構是一樣的,只是具體存儲的信息不同,用途也不同。元類對象存儲的信息主要包括:
- isa 指針
- superclass 指針
- 類方法(
+
開頭的方法)信息 - 其他一些信息
獲取元類對象也是調用object_getClass()
函數,只是傳入參數是類對象。換句話說object_getClass()
函數傳入的是實例對象的話就返回類對象,傳入的是類對象的話就返回元類對象。
// 獲取元類對象
Class metaClass = object_getClass([Student class]);
// 判斷某個對象是否是元類對象
BOOL isMetaClass = class_isMetaClass([Student class]);
這里要注意[[Student class] class]
這種寫法,[Student class]
返回的是類對象,那類對象再調用class方法是不是就返回的是元類對象呢?答案是否定的,一個類對象調用class方法返回的就是它自身。
3. isa指針和superclass指針
從前面介紹可以看出,所有繼承自NSObject的對象都有isa指針,所有類對象和元類對象都有superclass
指針。那這兩種指針到底有什么用呢?
我們首先來了解一下OC的方法調用原理,這屬于runtime的知識,這里只是簡單介紹一下,不做深入講解。調用OC方法底層是通過c語言的發送消息機制來實現的,比如一個實例對象stu調用study方法[stu study]
,其底層就是給stu對象發送消息(objc_msgSend(stu, @selector(study))
)。但是study方法的相關信息并不是存儲在實例對象中,而是在類對象中,那實例對象如何查找到study方法呢?這里isa指針就起作用了,實例對象的isa指針就是指向實例所屬的類對象的(嚴格來說,isa指針并不是一個普通的指針,它里面存儲的信息除了類對象的地址外,還包括很多其他信息,這里不做深入講解,我們簡單理解為實例對象的isa就是指向類對象即可)。
實例對象通過isa指針找到了類對象,然后在類對象中查找study方法并執行。但是如果study方法是Student
的父類實現的,那么在Student
類中是找不到study方法的,此時就要根據superclass指針
找到父類對象(superclass指針存儲的就是父類的地址,這和isa指針是不一樣的),如果父類也找不到那就繼續沿著繼承鏈進行查找。如果一直找到NSObject基類都沒找到的話,就會拋出unrecognized selector
異常(這里不考慮runtime的消息轉發)。
對于類方法的調用也是一樣的流程,只不過是從給實例對象發消息變成了給類對象發消息。類對象會根據自己的isa指針找到元類對象,然后在元類對象中查找類方法,查找不到也是根據元類的superclass指針沿著繼承鏈查找。
isa指針和superclass指針的指向可以總結為下面一張圖:
- 實例對象的isa指針指向該對象的類,該對象的實例方法保存在這個類的繼承鏈中;
- 類對象的isa指針指向該類的元類,類方法保存在元類的繼承鏈中;
- 元類和普通類一樣也有父類,也具備自己的繼承關系鏈,一個元類的父類就是這個元類的
類的父類
的元類(有點繞); - 所有元類的isa指針都是指向元類的根類(包括元類的根類的isa指針也是指向它自己);
- 注意根元類的
super_class
指針指向的是根類(NSObject)。
最后一點要格外注意,舉個例子,如果一個以NSObject為基類的類MyClss
,MyClass中聲明了一個類方法+(void)myTest;
,但是并沒有實現這個類方法(整個繼承鏈上都沒有實現),如果我們調用[MyClass myTest]
的話是會報unrecognized selector
異常的。
但是,如果我們給NSObject添加一個分類,在分類中實現了一個實例方法-(void)myTest;
,此時再調用[MyClass myTest]
的話時能正常運行的,而且執行的就是分類中添加的實例方法-(void)myTest;
。這個其實可以用上面那張圖進行解釋:首先MyClass類對象會根據其isa指針找到其元類對象,然后在元類對象和元類的繼承鏈上進行查找,一直查找到根元類對象都沒有找到一個名叫myTest
的方法,然后跟元類又會沿著其superclass指針找到NSObject類對象,而NSObject類對象中剛好有個叫myTest
的方法,所以就直接執行這個方法。
4. 類對象和元類對象的存儲結構(objc_class)
前面已經提過,不管是類對象還是元類對象,它們在內存中的存儲結構是一樣的。相關信息在objc4
源碼中。下面我會列出一些關鍵信息,想要了解完整信息可以去查看源碼。
首先我們來看下objc_class
這個結構體(這是c++語法,結構體可以繼承也可以在結構體里面定義函數),這個結構體我只列出了部分信息:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
}
可見objc_class
繼承自objc_object
,而objc_object
結構體里面就只有isa
這一個成員。我們再來看看objc_class
里面的內容:
-
superclass
指針 -
cache
,方法緩存。是cache_t
的結構體,這個結構體的定義可以去看源碼。 -
bits
是一個class_data_bits_t
結構體,這個結構體詳細信息可以去看源碼,這里我們主要介紹它后面的那個函數bits.data()
,看源碼可知,這個函數的實現其實就是bits & FAST_DATA_MASK)
,這個操作就是取出bits的某些位得到的就是一個指向結構體的指針,也就是class_rw_t
這個結構體。
下面我們來看看class_rw_t
這個結構體(rw其實就是readwrite的意思,也就是表示類中可讀可寫的信息):
struct class_rw_t {
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_ro_t
這個結構體(ro其實就是readonly的意思,也就是表示類中只讀的信息):
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; // 屬性列表
}
我們發現在class_rw_t
和class_ro_t
都有方法列表、屬性列表和協議列表,比如在class_rw_t
中的方法列表是methods
,在class_ro_t
中的方法列表是baseMethodList
,那這兩個有什么區別呢?class_ro_t
的初始化是在編譯的過程中完成的,對于一個類對象來說,編譯完成后,class_ro_t
中的baseMethodList
存著實例方法列表,這部分內容是不可以修改的,當class_rw_t
進行初始化時,會先將baseMethodList
拷貝放入methods
中,之后程序運行過程中動態添加的方法也是存放在methods
中。對于屬性列表和協議列表也是一樣的。