本次講解的不少內容都涉及到objc的源碼,有興趣的能夠去下載最新版本的objc4源碼。c++
咱們平時開發中說用到了絕大多數的類都是以NSObject
做爲基類。咱們進入NSObject.h
文件能夠看到NSObject
類的定義以下:緩存
@interface NSObject <NSObject> {
Class isa ;
}
複製代碼
咱們將OC代碼轉成c/c++代碼後能夠看到,NSObject
類是經過結構體
來實現的,以下所示:bash
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
文件中)的調用流程:app
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(這是系統硬性規定)。另外要注意的是一個實例對象佔用多少內存和類中是否有方法是沒有關係的,由於類中的方法並不存放在實例對象中。性能
咱們先定義一個繼承自NSObject
的Student
類,Student
類聲明瞭2個int類型屬性:測試
@interface Student : NSObject
@property (nonatomic , assign) int age;
@property (nonatomic , assign) int height;
@end
複製代碼
咱們將其轉換爲c/c++
代碼查看其底層結構:ui
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讀取數據時不是一個字節一個字節進行讀取的,而是每次讀取一塊數據,塊的大小在不一樣的系統上是不同的,能夠是二、四、八、16個字節。好比說若是CPU一次讀取16個字節,若是一個對象佔用內存的大小不是16的倍數,那麼CPU讀取這個對象數據時就須要作一些額外的操做,影響CPU的性能。
前面提到的對象都是實例對象,OC中除了實例對象
以外,還有另外兩種對象:類對象
和元類對象
。
實例對象就是經過類alloc出來的對象,好比Student *student = [[Student alloc] init];
,這樣就建立了Student類的一個實例對象,每次調用alloc都會產生新的實例對象。
一個實例對象在內存中存儲的信息前面也提到了,它的內存結構是比較簡單的,就只存了實例對象的全部成員變量的數據:
OC中每一個類都有一個與之對應的類對象,並且有且只有一個類對象
。與實例對象相比,類對象的內存結構要複雜不少(關於類的底層數據結構後面再作介紹),其在內存中存儲的信息主要包括:
-
開頭的方法)信息獲取類對象的方法有多種,無論哪種方法獲取到的類對象都是同樣的。
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);
複製代碼
從上面介紹咱們能夠看出,類對象是用來存儲實例對象的信息的(好比實例方法、屬性等信息),那類對象的信息(好比類的類方法信息)又是存在哪裏呢?這就是咱們要介紹的元類對象
。
每一個類在內存中有且只有一個元類對象
,元類對象和類對象的內存結構是同樣的,只是具體存儲的信息不一樣,用途也不一樣。元類對象存儲的信息主要包括:
+
開頭的方法)信息獲取元類對象也是調用object_getClass()
函數,只是傳入參數是類對象。換句話說object_getClass()
函數傳入的是實例對象的話就返回類對象,傳入的是類對象的話就返回元類對象。
// 獲取元類對象
Class metaClass = object_getClass([Student class]);
// 判斷某個對象是不是元類對象
BOOL isMetaClass = class_isMetaClass([Student class]);
複製代碼
這裏要注意[[Student class] class]
這種寫法,[Student class]
返回的是類對象,那類對象再調用class方法是否是就返回的是元類對象呢?答案是否認的,一個類對象調用class方法返回的就是它自身。
從前面介紹能夠看出,全部繼承自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指針的指向能夠總結爲下面一張圖:
類的父類
的元類(有點繞);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
的方法,因此就直接執行這個方法。
前面已經提過,不論是類對象仍是元類對象,它們在內存中的存儲結構是同樣的。相關信息在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
中。對於屬性列表和協議列表也是同樣的。