本文基于objc4-709源碼進行分析。關(guān)于源碼編譯:objc - 編譯Runtime源碼objc4-706
objc中的類和對象
1.類和對象的結(jié)構(gòu)概要
NSObject是所有類的基類,NSObject在源碼中的定義:
在NSObject.mm文件中找到
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
NSObject類的第一個成員變量就是Class類型的isa。
Object.mm文件:
typedef struct objc_class *Class; //類
typedef struct objc_object *id;
@interface Object {
Class isa;
}
Class就是c語言定義的objc_class結(jié)構(gòu)體類型的指針,objc中的類實際上就是objc_class。
而id類型就是objc_object結(jié)構(gòu)體類型的指針(就是我們平時經(jīng)常用到的id類型),我們平時用的id可以用來聲明一個對象,說明objc中的對象實際上就是objc_object。
objc-runtime-new.h
struct objc_class : objc_object {
// Class ISA;
Class superclass; //父類的指針
cache_t cache; // formerly cache pointer and vtable 方法緩存
class_data_bits_t bits; // class
...
}
objc_class繼承于objc_object,objc中的類也是一個對象。
objc-private.h
struct objc_object {
private:
isa_t isa;//objc_object唯一成員變量
public:
Class ISA();
Class getIsa();
void initIsa(Class cls /*nonpointer=false*/);
...
private:
void initIsa(Class newCls, bool nonpointer, bool hasCxxDtor);
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
...
}
objc_object是objc中對象的定義。isa 是 objc_object的唯一成員變量。我們經(jīng)常說,所有的objc對象都包含一個isa指針,從源碼上來看,現(xiàn)在準確說應(yīng)該是isa_t結(jié)構(gòu)體(isa指針應(yīng)該是isa_t聯(lián)合體中的Class cls
指針)。當objc為一個對象分配內(nèi)存,初始化實例變量后,在這些對象的實例變量的結(jié)構(gòu)體中的第一個就是isa。
為了方便閱讀,我把objc_class寫成:
struct objc_class : objc_object {
isa_t isa;
Class superclass; //父類的指針
cache_t cache; // formerly cache pointer and vtable 方法緩存
class_data_bits_t bits; // class
...
}
在objc中,對象的方法的實現(xiàn)不會存儲在每個對象的結(jié)構(gòu)體中,而是在相應(yīng)的類里(如果每一個對象都要維護一個實例方法列表,那么開銷太大了)。當一個實例方法被調(diào)用時,會通過對象的isa,在對應(yīng)的類中找到方法的實現(xiàn)(具體是在class_data_bits_t結(jié)構(gòu)體中查找,里面有一個方法列表)。
同時我們還從源碼中看到,objc_class結(jié)構(gòu)體中還有一個Class類型的superclass成員變量,指向了父類。通過這個指針可以查找從父類繼承的方法。
然而,對于類對象來說,它的isa又是什么呢?objective-c里面有一種叫做meta class元類的東西。
為了讓我們能夠調(diào)用類方法,類的isa“指針”必須指向一個類結(jié)構(gòu),并且該類結(jié)構(gòu)必須包含我們可以調(diào)用的類方法列表。這就導(dǎo)致了元類的定義:元類是類對象的類。
類方法調(diào)用時,通過類的isa“指針”在元類中獲取方法的實現(xiàn)。元類中存儲了一個類的所有類方法。
從上圖中總結(jié)以下幾個信息:
- root class(class)就是NSObject,由于它是基類,所以它沒有父類。
- 實例對象的isa指向其類,類的isa指向其元類。每個元類的isa都指向根元類root class(meta),根元類的isa指向自己。
- 根元類的父類指針指向基類(NSObject)。
What is a meta-class in Objective-C?
ps:isa指針并不總是指向?qū)嵗龑ο笏鶎俚念悾荒芤揽克鼇泶_定類型,而應(yīng)用class方法。(KVO的實現(xiàn)中,將被觀察的對象的isa指針指向一個中間類而非真實的類)。
2.isa_t結(jié)構(gòu)體的分析
通過源碼,我們可以知道isa_t實際上是一個union聯(lián)合體。其中的方法、成員變量、結(jié)構(gòu)體公用一塊空間。取決于其中的結(jié)構(gòu)體,最終isa_t共占64位內(nèi)存空間
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;//指針
uintptr_t bits;
#if SUPPORT_PACKED_ISA
//-------------------arm64上的實現(xiàn)
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL//ISA_MAGIC_MASK 和 ISA_MASK 分別是通過掩碼的方式獲取MAGIC值 和 isa類指針
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; //0:普通isa指針,1:優(yōu)化的指針用于存儲引用計數(shù)
uintptr_t has_assoc : 1; //表示該對象是否包含associated object,如果沒有,析構(gòu)時會更快
uintptr_t has_cxx_dtor : 1; //表示對象是否含有c++或者ARC的析構(gòu)函數(shù),如果沒有,析構(gòu)更快
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 類的指針
uintptr_t magic : 6; //用于在調(diào)試時分辨對象是否未完成初始化(用于調(diào)試器判斷當前對象是真的對象還是沒有初始化的空間)
uintptr_t weakly_referenced : 1; //對象是否有過weak對象
uintptr_t deallocating : 1; //是否正在析構(gòu)
uintptr_t has_sidetable_rc : 1; //該對象的引用計數(shù)值是否過大無法存儲在isa指針
uintptr_t extra_rc : 19; //存儲引用計數(shù)值減一后的結(jié)果。對象的引用計數(shù)超過 1,會存在這個這個里面,如果引用計數(shù)為 10,extra_rc的值就為 9。
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
//-------------------__x86_64__上的實現(xiàn)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t magic : 6; //值為0x3b
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};
# else
# error unknown architecture for packed isa
# endif
#endif
...略
由于源碼要在mac os下才能編譯,因此接下來都基于x86_64分析,arm64其實大同小異。
源碼中遇到的編譯宏的說明:
SUPPORT_PACKED_ISA
:表示平臺是否支持在 isa 指針中插入除 Class 之外的信息。如果支持就會將 Class 信息放入 isa_t 定義的 struct 內(nèi)(shiftcls),并附上一些其他信息,比如引用計數(shù),析構(gòu)狀態(tài);如果不支持,那么不會使用 isa_t 內(nèi)定義的 struct,這時 isa_t 只使用 cls成員變量(Class 指針,經(jīng)常說的“isa指針”就是這個)。在 iOS 以及 MacOSX 上,SUPPORT_PACKED_ISA 定義為 1(支持)。
struct結(jié)構(gòu)體的成員含義:
參數(shù) 含義
nonpointer 0 表示普通的 isa 指針,1 表示使用優(yōu)化,存儲引用計數(shù)
has_assoc 表示該對象是否包含 associated object 關(guān)聯(lián)對象,如果沒有,則析構(gòu)時會更快
has_cxx_dtor 表示該對象是否有 C++ 或 ARC 的析構(gòu)函數(shù),如果沒有,則析構(gòu)時更快
shiftcls 類的指針
magic __x86_64__環(huán)境初始化值為 0x3b,用于在調(diào)試時分辨對象是否未完成初始化。
weakly_referenced 表示該對象是否有過 weak 對象,如果沒有,則析構(gòu)時更快
deallocating 表示該對象是否正在析構(gòu)
has_sidetable_rc 表示該對象的引用計數(shù)值是否過大無法存儲在 isa 指針
extra_rc 存儲引用計數(shù)值減一后的結(jié)果。19位將保存對象的引用計數(shù),這樣對引用計數(shù)的操作只需要原子的修改這個指針即可,如果引用計數(shù)超出19位,才會將引用計數(shù)保存到外部表,而這種情況往往是很少的,因此效率將會大大提高。
在初始化一節(jié)會對其中一些參數(shù)繼續(xù)談?wù)劇?/p>
初始化
在我們調(diào)用alloc來實例化對象時,會調(diào)用到objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
方法。方法的具體實現(xiàn):
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
if (!nonpointer) {
isa.cls = cls;
} else {
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
//__x86_64__和arm64會進入這個條件分支
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
initInstanceIsa
方法實際上調(diào)用了initIsa
方法,并且傳入nonpointer=true
參數(shù)。
由于isa_t是一個聯(lián)合體,所以newisa.bits = ISA_MAGIC_VALUE;
把isa_t中的struct結(jié)構(gòu)體初始化為:
實際上只設(shè)置了nonpointer和magic的值。
nonpointer
nonpointer 表示是否對isa開啟指針優(yōu)化。
先介紹一種稱為Tagged Pointer的技術(shù),這是蘋果為64位設(shè)備提出的節(jié)省內(nèi)存和提高執(zhí)行效率的一種優(yōu)化方案。
假設(shè)我們要存儲一個NSNumber對象,其值是一個整數(shù)。正常情況下,如果這個整數(shù)只是一個NSInteger的普通變量,那么它所占用的內(nèi)存是與CPU的位數(shù)有關(guān),在32位CPU下占4個字節(jié),在64位CPU下是占8個字節(jié)的。而指針類型的大小通常也是與CPU位數(shù)相關(guān),一個指針所占用的內(nèi)存在32位CPU下為4個字節(jié),在64位CPU下也是8個字節(jié)。
如果沒有Tagged Pointer對象,從32位機器遷移到64位機器中后,雖然邏輯沒有任何變化,但這種NSNumber、NSDate一類的對象所占用的內(nèi)存會翻倍。
為了改進上面提到的內(nèi)存占用和效率問題,蘋果提出了Tagged Pointer對象。由于NSNumber、NSDate一類的變量本身的值需要占用的內(nèi)存大小常常不需要8個字節(jié),拿整數(shù)來說,4個字節(jié)所能表示的有符號整數(shù)就可以達到20多億。所以我們可以將一個對象的指針拆成兩部分,一部分直接保存數(shù)據(jù),另一部分作為特殊標記,表示這是一個特別的指針,不指向任何一個地址。
技術(shù)詳細可以查看以下兩個鏈接。
雖然Tagged Pointer專門用來存儲小的對象,例如NSNumber和NSDate。但在isa指針優(yōu)化上用到了tagged Pointer的概念。
使用整個指針大小的內(nèi)存來存儲isa有些浪費,在arm64上運行的iOS只用了33位(和結(jié)構(gòu)體中shiftcls的33無關(guān),Mac OS用了47位),剩下的31位用于其他目的。
nonpointer = 0 ,raw isa,表示isa_t不使用struct結(jié)構(gòu)體,訪問對象的 isa 會直接返回 cls 指針,cls 會指向?qū)ο笏鶎俚念惖慕Y(jié)構(gòu)。這是在 iPhone 遷移到 64 位系統(tǒng)之前時 isa 的類型。
nonpointer = 1,isa_t使用了struct結(jié)構(gòu)體,此時 isa 不再是指針,不能直接訪問 objc_object 的 isa 成員變量,但是其中也有 cls 的信息,只是其中關(guān)于類的指針都是保存在 shiftcls 中。
bool hasNonpointerIsa();
這個函數(shù)就是用來判斷當前對象的isa是否啟用tagged pointer的。
magic
用于判斷當前對象是否已經(jīng)完成初始化。
has_cxx_dtor
接著設(shè)置has_cxx_dtor,這一位表示當前對象是否有 C++ 或者 ObjC 的析構(gòu)器,如果沒有析構(gòu)器就會快速釋放內(nèi)存。
shiftcls
newisa.shiftcls = (uintptr_t)cls >> 3;
將對象對應(yīng)的類的指針存入結(jié)構(gòu)體的shiftcls成員中。
將cls(地址)右移三位的原因:字節(jié)對齊。類的指針按照8字節(jié)對齊。
對象的內(nèi)存地址必須對齊到字節(jié)的倍數(shù),這樣可以提高代碼運行的性能,在 iPhone5s 中虛擬地址為 33 位,所以用于對齊的最后三位比特為 000,我們只會用其中的 30 位來表示對象的地址。
將cls右移三位,可以將地址中無用的后三位清除減小內(nèi)存的消耗。
回過頭來看,類的isa的shiftcls就指向元類了。
ISA()
由于開啟了指針優(yōu)化后,isa不再是指針,要獲取類指針就要用到 ISA() 方法。
#define ISA_MASK 0x00007ffffffffff8ULL
inline Class
objc_object::ISA()
{
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK); //按位與運算獲取類指針,__x86_64__是44位
#endif
}
3.cache_t 結(jié)構(gòu)體的分析
typedef unsigned int uint32_t;
typedef uint32_t mask_t;
typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
......
};
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
......
};
cache 存放著實例方法的緩存,提高方法調(diào)用效率。當一個對象調(diào)用一個方法時,它會先從緩存里面找這個方法,如果沒有找到才會去類的方法列表中去找。
如果一個對象調(diào)用一個方法,首先根據(jù)對象的isa找到對應(yīng)的類,再在類的方法列表中尋找這個方法,如果找不到就到父類中的方法列表查找,一旦找到就調(diào)用。如果沒有找到,有可能轉(zhuǎn)發(fā)消息,也可能忽略它。但這樣效率太低了,有些方法會經(jīng)常用到,那么每次調(diào)用都要走一遍以上流程,是不是很慢?用cache來緩存方法,優(yōu)先在 cache 中查找,找到就調(diào)用沒找到再走正常路子。
cache_t中存儲了一個bucket_t結(jié)構(gòu)體指針和兩個無符號整形變量。
bucket_t結(jié)構(gòu)體中存儲了IMP函數(shù)指針和unsigned long。_buckets 用來存儲Method的鏈表。
_mask 分配用來緩存bucket的總數(shù)。
_occupied 當前占用的bucket數(shù)。
4.class_data_bits_t
之前說過,實例方法被調(diào)用時,會通過其持有 isa 指針尋找對應(yīng)的類,然后在其中的 class_data_bits_t 中查找對應(yīng)的方法。相對來說 class_data_bits_t 會有點復(fù)雜,包含了 class_rw_t 、class_ro_t 等重要的結(jié)構(gòu)體。這一節(jié)會對 class_data_bits_t 進行分析,介紹方法是如何在objc中存取的。
struct class_data_bits_t {
uintptr_t bits;
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
......
};
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();
}
......
};
class_data_bits_t 結(jié)構(gòu)體只含有一個64bit的bits變量,它存儲了類相關(guān)的信息,和不同的以'FAST_'開頭的flag掩碼做按位與運算。
uintptr_t bits存儲了一個指向 class_rw_t 結(jié)構(gòu)體的指針和三個標志位。bits在64位兼容版中的內(nèi)容:
// 當前類是swift類
#define FAST_IS_SWIFT (1UL<<0)
//當前類或者父類含有默認的 retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法
#define FAST_HAS_DEFAULT_RR (1UL<<1)
// 當前類的實例需要 raw isa
#define FAST_REQUIRES_RAW_ISA (1UL<<2)
// 數(shù)據(jù)指針
#define FAST_DATA_MASK 0x00007ffffffffff8UL
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
在 objc_class 的 data 方法中,直接調(diào)用并返回了 class_data_bits_t 的data方法。bits 和 FAST_DATA_MASK做按位與運算,轉(zhuǎn)換為 class_rw_t *并返回。
Mac OS X 只使用 47 位內(nèi)存地址,所以bits前 17 位空余出來。bits中最大的一塊存儲區(qū)域用于存儲class_rw_t 指針,在 FAST_DATA_MASK 對應(yīng)的[3,46]位數(shù)據(jù)段中。[3,46]一共是44位,有沒有覺得44很眼熟?由于字節(jié)對齊,所以47位的后三位為0,可以用來做標志位,所以就有了另外三個標志位。
除了 FAST_DATA_MASK 是用一段空間做存儲外,其他宏都是用1bit。這些數(shù)據(jù)都有對應(yīng)的getter、setter封裝函數(shù)。比如:
bool isSwift() {return getBit(FAST_IS_SWIFT);}
void setIsSwift() {setBits(FAST_IS_SWIFT);}
bool hasDefaultRR() {return getBit(FAST_HAS_DEFAULT_RR);}
void setHasDefaultRR() {setBits(FAST_HAS_DEFAULT_RR);}
在64非兼容版本下還有更多的宏定義,位于objc-runtime-new.h文件中,如有需要請自行閱讀源碼。
class_rw_t
從上面的分析中我們知道,class_data_bits_t 用了大段空間存儲了 class_rw_t 指針。class_rw_t 提供了運行時對類擴展的能力,class_rw_t 結(jié)構(gòu)體中保存了 objc 中的屬性、方法、協(xié)議等信息。
在 objc_class結(jié)構(gòu)體中的注釋寫到 class_data_bits_t相當于 class_rw_t指針加上 rr/alloc 的標志。
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
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;//協(xié)議
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;//是計算機語言用于解決實體名稱唯一性的一種方法,做法是向名稱中添加一些類型信息,用于從編譯器中向連接器傳遞更多語義信息。
......
};
關(guān)于32位 flags 標志,在源碼中有這樣一段注釋:
Values for class_rw_t->flags
These are not emitted by the compiler and are never used in class_ro_t.
Their presence should be considered in future ABI versions.
它的值不是由編譯器設(shè)置的,并且從不在class_ro_t中使用。未來的ABI版本應(yīng)考慮到他們的存在。
flags 標記了類的一些狀態(tài),與生命周期、內(nèi)存管理有關(guān),有些位目前還沒有定義。
class_rw_t 中的 methods、properties、protocols 存放了和類的方法、屬性、協(xié)議有關(guān)的信息。
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
class method_array_t : public list_array_tt<method_t, method_list_t>
class property_array_t : public list_array_tt<property_t, property_list_t>
class protocol_array_t : public list_array_tt<protocol_ref_t, protocol_list_t>
method_array_t、property_array_t、protocol_array_t這三種類都繼承自 list_array_tt<Element, List>這種結(jié)構(gòu)。
template <typename Element, typename List>
class list_array_tt {
struct array_t {
uint32_t count;
List* lists[0];//長度為0的數(shù)組,c99的寫法,允許在運行期動態(tài)申請內(nèi)存
};
}
list_array_tt 存儲一些元數(shù)據(jù),是通過c++模板定義的容器類,提供了一些諸如count、迭代器iterator的方法和類。Element表示元數(shù)據(jù)的類型,比如 method_t 、 property_t 、 protocol_ref_t;List表示存儲元數(shù)據(jù)的容器,理解成是用于存儲元數(shù)據(jù)的一維數(shù)組,比如 method_list_t 、 property_list_t 。由于list_array_tt 存儲了List指針數(shù)組,所以list_array_tt實際上可以看做是元數(shù)據(jù)的二維數(shù)組。list_array_tt 有三種狀態(tài):
- 自身為空
- List指針數(shù)組只有一個指向元數(shù)據(jù)數(shù)組的指針
- List指針數(shù)組有多個指針
一個類創(chuàng)建之初可能處于前兩個狀態(tài),如果用category或者class_addMethod來添加方法,就變成第三個狀態(tài),而且是不可逆的回不去前兩個狀態(tài)
可以對list_array_tt不斷進行擴張。比如在通過category添加方法時,就調(diào)用到這個方法,把新的方法列表(相當于是裝有一個category所有方法的容器)添加到二維數(shù)組中:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
//調(diào)用realloc將原空間擴展,把原數(shù)組復(fù)制到后面,新數(shù)組復(fù)制到前面
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
//List指針數(shù)組只有一個指針時
//malloc重新申請內(nèi)存,最后一個位置留給原來元數(shù)據(jù)數(shù)組的指針
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
無論是哪一種邏輯,新加的方法列表都會添加到二維數(shù)組前面。
有一個名字很類似的結(jié)構(gòu)體 class_ro_t,'rw' 和 ro' 相信很容易就理解是 'readwrite' 和 'readonly'的意思吧。
class_rw_t 的結(jié)構(gòu)體成員中還有一個指向常量的指針class_ro_t *ro
。因此在編譯期間類的結(jié)構(gòu)中的 class_data_bits_t *data 指向的是一個 class_ro_t * 指針。class_ro_t也是一個結(jié)構(gòu)體,下一節(jié)我們來講講它。
class_ro_t
class_rw_t 的內(nèi)容可以在運行時動態(tài)修改,而 class_ro_t 存儲了在類編譯時就確定的信息,比如屬性、方法、協(xié)議、成員變量。
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;//協(xié)議列表
const ivar_list_t * ivars;//ivar列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;//屬性列表
method_list_t *baseMethods() const {
return baseMethodList;
}
};
在類進行初始化之前,通過object_class 的data()方法得到的實際是一個指向 class_ro_t 結(jié)構(gòu)體的指針。
instanceStart、instanceSize 兩個成員變量的存在保證了objc2.0的ABI穩(wěn)定性。Non Fragile ivars
method_list_t、ivar_list_t、property_list_t 結(jié)構(gòu)體都繼承自 entsize_list_tt<Element, List, FlagMask> , protocol_list_t 的結(jié)構(gòu)則相對簡單很多。entsize_list_tt 是通過c++模板定義的容器類,提供了一些諸如count、get、迭代器iterator的方法和類,通過這些方法和類可以方便地遍歷并獲取容器內(nèi)的數(shù)據(jù)。
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
uint32_t entsizeAndFlags;//總大小
uint32_t count;//個數(shù)
Element first;//第一個元數(shù)據(jù)
};
而在類進行初始化之時,會調(diào)用到一個 static Class realizeClass(Class cls)
方法(這個之后分析oc中的消息發(fā)送時會具體分析,這里只要知道會調(diào)用到這個方法即可)。
realizeClass的實現(xiàn)節(jié)選:
ro = (const class_ro_t *)cls->data();//強制轉(zhuǎn)換
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);//初始化一個 class_rw_t 結(jié)構(gòu)體,分配可讀寫數(shù)據(jù)空間
rw->ro = ro;//設(shè)置結(jié)構(gòu)體 ro 的值以及 flag
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);//最后設(shè)置正確的 data
}
這里主要做了幾個事:
- 調(diào)用 object_class 的 data 方法,把得到的 class_rw_t 指針強制轉(zhuǎn)換成 class_ro_t 指針。
- 新初始化一個 class_rw_t ,分配可讀寫數(shù)據(jù)空間。
- 將 class_ro_t 指針賦值給 class_rw_t->ro ,并設(shè)置 class_rw_t->flag 。
- 設(shè)置 object_class 的正確 data 。
也即由以下圖1變成了圖2:圖片來源深入解析 ObjC 中方法的結(jié)構(gòu)
但是此時 class_rw_t 中的方法,屬性以及協(xié)議列表均為空。最后在 realizeClass 方法中 調(diào)用了 methodizeClass 方法來將類 class_ro_t 的方法、屬性和遵循的協(xié)議加載到 methods、 properties 和 protocols 列表中(用到了在 class_rw_t 一節(jié)最后提到的 attachLists 方法),但是class_ro_t其中的 ivar 不會被拷貝(不能在運行時給已有的類增加實例變量)。methodizeClass 方法節(jié)選:
// Install methods and properties that the class implements itself.
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rw->properties.attachLists(&proplist, 1);
}
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rw->protocols.attachLists(&protolist, 1);
}
可以這樣說,經(jīng)過realizeClass
函數(shù)處理的類才成為了真正的類。
5.驗證一下
一下這part的內(nèi)容基于Draveness大大《深入解析 ObjC 中方法的結(jié)構(gòu)》一文中驗證部分進行復(fù)現(xiàn),分析運行時初始化過程的內(nèi)存變化,作為自己敲一下代碼所做的記錄。
#import <Foundation/Foundation.h>
@interface XXObject : NSObject
@property (nonatomic, assign) CGFloat test;
- (void)hello;
@end
#import "XXObject.h"
@implementation XXObject
- (void)hello {
NSLog(@"Hello");
}
@end
主程序代碼:
#import <Foundation/Foundation.h>
#import "XXObject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Class cls = [XXObject class];
NSLog(@"%p", cls);
}
return 0;
}
由于類在內(nèi)存中的位置是編譯期就確定的,在之后修改代碼,也不會改變內(nèi)存中的位置。所以先跑一次代碼獲取XXObject 在內(nèi)存中的地址。地址為:0x1000011e0
在運行時初始化之前加入斷點:
然后通過lldb驗證一下:
(lldb) p (objc_class *)0x1000011e0
(objc_class *) $0 = 0x00000001000011e0
(lldb) p (class_data_bits_t *)0x100001200 //偏移32為獲取class_data_bits_t指針
(class_data_bits_t *) $1 = 0x0000000100001200
(lldb) p $1->data()
warning: could not find Objective-C class data in the process. This may reduce the quality of type information available.
(class_rw_t *) $2 = 0x0000000100001158
(lldb) p (class_ro_t *)$2 //強制轉(zhuǎn)換類型
(class_ro_t *) $3 = 0x0000000100001158
(lldb) p *$3
(class_ro_t) $4 = {
flags = 128
instanceStart = 8
instanceSize = 16
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x0000000100000f6e "XXObject"
baseMethodList = 0x00000001000010c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000100001118
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000100001140
}
可以看到name、baseMethodList、ivars、baseProperties是有值的,對于后兩者有值的原因:因為XXObject類中我添加了@property (nonatomic, assign) CGFloat test;
屬性,而屬性也在編譯期自動生成了一個對應(yīng)的ivar(帶下劃線前綴的原屬性名的成員變量)。
//獲取ivars
(lldb) p $4.ivars
(const ivar_list_t *) $6 = 0x0000000100001118
(lldb) p *$6
//顯示count=1只有一個ivar
(const ivar_list_t) $7 = {
entsize_list_tt<ivar_t, ivar_list_t, 0> = {
entsizeAndFlags = 32
count = 1
first = {
offset = 0x00000001000011b0
name = 0x0000000100000f8b "_test"
type = 0x0000000100000fb2 "d"
alignment_raw = 3
size = 8
}
}
}
(lldb) p $7.get(0)
(ivar_t) $8 = {
offset = 0x00000001000011b0
name = 0x0000000100000f8b "_test"
type = 0x0000000100000fb2 "d"
alignment_raw = 3
size = 8
}
//獲取property
(lldb) p $4.baseProperties
(property_list_t *) $10 = 0x0000000100001140
(lldb) p *$10
(property_list_t) $11 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 1
first = (name = "test", attributes = "Td,N,V_test")
}
}
查看 baseMethodList 中的內(nèi)容:
(lldb) p $4.baseMethodList
(method_list_t *) $14 = 0x00000001000010c8
(lldb) p *$14
(method_list_t) $15 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 24
count = 3
first = {
name = "hello"
types = 0x0000000100000f97 "v16@0:8"
imp = 0x0000000100000e20 (debug-objc`-[XXObject hello] at XXObject.m:11)
}
}
}
(lldb) p $15.get(0)
(method_t) $16 = {
name = "hello"
types = 0x0000000100000f97 "v16@0:8"
imp = 0x0000000100000e20 (debug-objc`-[XXObject hello] at XXObject.m:11)
}
(lldb) p $15.get(1)
(method_t) $17 = {
name = "test"
types = 0x0000000100000f9f "d16@0:8"
imp = 0x0000000100000e50 (debug-objc`-[XXObject test] at XXObject.h:11)
}
(lldb) p $15.get(2)
(method_t) $18 = {
name = "setTest:"
types = 0x0000000100000fa7 "v24@0:8d16"
imp = 0x0000000100000e70 (debug-objc`-[XXObject setTest:] at XXObject.h:11)
}
結(jié)果顯示 baseMethodList 中有三個方法,打印出來看,第一個是 XXObject 的 hello 方法,其余兩個是 test 屬性的對應(yīng)getter和setter方法。所以也就不難理解為什么objc中的property = ivar + getter + setter了。
關(guān)于"v16@0:8"
、"d16@0:8"
、"v24@0:8d16"
請見Type Encoding
以上這些都是在編譯器就生成了的。
接著來看一下如果沒有把class_rw_t強轉(zhuǎn)成class_ro_t時的情形:先打印出來留到后面與初始化之后進行對比。
(lldb) p *$2
(class_rw_t) $5 = {
flags = 128
version = 8
ro = 0x0000000000000010
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000100000f6e
arrayAndFlag = 4294971246
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x00000001000010c8
arrayAndFlag = 4294971592
}
}
}
firstSubclass = nil
nextSiblingClass = 0x0000000100001118
demangledName = 0x0000000000000000 <no value available>
}
接著在 realizeClass 方法中的這個地方打一個斷點:(直接和地址值進行比較是因為類在編譯期就確定了在內(nèi)存中的地址)
判斷當前類是否XXObject,此時還沒有進行初始化,類結(jié)構(gòu)體中的布局依舊是上面那樣沒有變化的。
[圖片上傳失敗...(image-5a0ad-1517664314598)]
當這段代碼運行完后,再來lldb打印一下:
(lldb) p (objc_class *)0x1000011e0
(objc_class *) $19 = 0x00000001000011e0
(lldb) p (class_data_bits_t *)0x0000000100001200
(class_data_bits_t *) $20 = 0x0000000100001200
(lldb) p $20->data()
(class_rw_t *) $21 = 0x0000000100f05030
(lldb) p *$21
(class_rw_t) $22 = {
flags = 2148007936
version = 0
ro = 0x0000000100001158
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = nil
demangledName = 0x0000000000000000 <no value available>
}
(lldb) p $22.ro
(const class_ro_t *) $23 = 0x0000000100001158
(lldb) p *$23
(const class_ro_t) $24 = {
flags = 128
instanceStart = 8
instanceSize = 16
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x0000000100000f6e "XXObject"
baseMethodList = 0x00000001000010c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000100001118
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000100001140
}
可以看到,$3
的class_ro_t *
已經(jīng)被設(shè)置成$22
class_rw_t中的 ro 成員了,但 class_rw_t 的 methods、properties、protocols 成員
仍然為空。
等到執(zhí)行完 methodizeClass 方法:
(lldb) p *$21
(class_rw_t) $25 = {
flags = 2148007936
version = 0
ro = 0x0000000100001158
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000010c8
arrayAndFlag = 4294971592
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000100001140
arrayAndFlag = 4294971712
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fffcd723f50
demangledName = 0x0000000000000000 <no value available>
}
class_ro_t 中 baseMethodList 中的方法被添加到 class_rw_t 中的 methods 數(shù)組中,class_ro_t 中 baseProperties 被添加到 class_rw_t 中的 properties。核心都是通過在 methodizeClass 方法中執(zhí)行 attachLists 方法添加類的方法、屬性、協(xié)議:
auto rw = cls->data();
auto ro = rw->ro;
// Install methods and properties that the class implements itself.
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rw->properties.attachLists(&proplist, 1);
}
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rw->protocols.attachLists(&protolist, 1);
}
總結(jié)
類的方法、屬性以及協(xié)議在編譯期間存放到了“錯誤”的位置,直到 realizeClass 執(zhí)行之后,才放到了 class_rw_t 指向的只讀區(qū)域 class_ro_t,這樣我們即可以在運行時為 class_rw_t 添加方法,也不會影響類的只讀結(jié)構(gòu)。
在 class_ro_t 中的屬性在運行期間就不能改變了,再添加方法時,會修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethodList
6.對象的初始化
創(chuàng)建一個 NSObject 對象的代碼:[[NSObject alloc] init];
alloc
方法的實現(xiàn):
+ (id)alloc {
return _objc_rootAlloc(self);
}
僅僅調(diào)用了一個私有方法_objc_rootAlloc
:
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
// 檢查 cls 信息是否為 nil,如果為 nil,則無法創(chuàng)建新對象,返回 nil。
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
//是否有自定義的allocWithZone方法
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
//沒有alloc / allocWithZone實現(xiàn)。
//是否可以快速分配內(nèi)存。這里好像是寫死了返回false
if (fastpath(cls->canAllocFast())) {
bool dtor = cls->hasCxxDtor();//是否有析構(gòu)器
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
id obj = class_createInstance(cls, 0);// 創(chuàng)建對象的關(guān)鍵函數(shù)
if (slowpath(!obj)) return callBadAllocHandler(cls);// 分配失敗
return obj;
}
}
#endif
// callAlloc 傳入 allocWithZone = true
if (allocWithZone) return [cls allocWithZone:nil];// 這里 cls 的 allocWithZone 方法里也是調(diào)用了 class_createInstance。
return [cls alloc];
}
關(guān)于slowpath
、fastpath
兩個宏,實際上是__builtin_expect(),一個GCC的一個內(nèi)建函數(shù),用于分支預(yù)測優(yōu)化。__builtin_expect — 分支預(yù)測優(yōu)化。只需要知道if(fastpath(x))
與 if(x)
是相同意思就可以了。
實際上我在執(zhí)行[[XXObject alloc] init]
這句代碼時,if (fastpath(!cls->ISA()->hasCustomAWZ()))
并沒有進入這個條件分支:
所以也即相當于:
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
return [cls allocWithZone:nil];
}
但很神奇的是,接下來并沒有執(zhí)行allocWithZone
方法(打了斷點但無事發(fā)生),這是一個讓我覺得很迷的地方。最后落入的是這兩個方法:
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();// 是否有構(gòu)造函數(shù)
bool hasCxxDtor = cls->hasCxxDtor();// 是否有析構(gòu)函數(shù)
bool fast = cls->canAllocNonpointer();// 是否使用原始 isa 格式
size_t size = cls->instanceSize(extraBytes);// 需要分配的空間大小,從 instanceSize 實現(xiàn)可以知道對象至少16字節(jié)
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {//跑代碼時會執(zhí)行這個分支
obj = (id)calloc(1, size);//分配空間
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);//看到了一個熟悉的方法~
}
else { //這里不執(zhí)行
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;
}
在上面函數(shù)的實現(xiàn)中,執(zhí)行了 objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
方法。在第二部分講過,這個方法就是用來初始化isa_t結(jié)構(gòu)體的。
初始化 isa 之后,[NSObject alloc]
的工作算是做完了,下面就是 init
相關(guān)邏輯:
- (id)init {
return _objc_rootInit(self);
}
id _objc_rootInit(id obj)
{
return obj;
}
可以看到, init
實際上只是返回了當前對象。
總結(jié)一下對象的初始化過程主要做了兩個事:1.分配需要的內(nèi)存空間 2.初始化isa_t結(jié)構(gòu)體。
參考文章: