注:本文始發于個人 GitHub 項目 ShannonChenCHN/iOSDevLevelingUp。
關于 objc4 源碼的一些說明:
- objc4 的源碼不能直接編譯,需要配置相關環境才能運行??梢栽?a target="_blank">這里下載可調式的源碼。
- objc 運行時源碼的入口在
void _objc_init(void)
函數。
目錄
- 1.Objective-C 對象是什么?Class 是什么?id 又是什么?
- 2.isa 是什么?為什么要有 isa?
- 3.為什么在 Objective-C 中,所以的對象都用一個指針來追蹤?
- 4.Objective-C 對象是如何被創建(alloc)和初始化(init)的?
- 5.Objective-C 對象的實例變量是什么?為什么不能給 Objective-C 對象動態添加實例變量?
- 6.Objective-C 對象的屬性是什么?屬性跟實例變量的區別?
- 7.Objective-C 對象的方法是什么?Objective-C 對象的方法在內存中的存儲結構是什么樣的?
- 8.什么是 IMP?什么是選擇器 selector ?
- 9.消息發送和消息轉發
- 10.Method Swizzling
- 11.Category
- 12.Associated Objects 的原理是什么?到底能不能在 Category 中給 Objective-C 類添加屬性和實例變量?
- 13.Objective-C 中的 Protocol 是什么?
- 14.
self
和super
的本質 - 15.
load
方法和initialize
方法
1. Objective-C 對象是什么?Class 是什么?id 又是什么?
所有的類都繼承 NSObject 或者 NSProxy,先來看看這兩個類在各自的公開頭文件中的定義:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
@interface NSProxy <NSObject> {
Class isa;
}
在 objc.h 文件中,對于 Class,id 以及 objc_object 的定義:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
runtime.h 文件中對 objc_class 的定義:
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;
在 Objective-C 中,每一個對象是一個結構體,每個對象都有一個 isa 指針,類對象 Class 也是一個對象。所以,我們說,凡是包含 isa 指針的,都可以被認為是 Objective-C 中的對象。運行時可以通過 isa 指針,查找到該對象是屬于什么類(Class)。
2. isa 是什么?為什么要有 isa?
在 Runtime 源碼中,對于 objc_object 和 objc_class 的定義分別如下:
struct objc_object {
private:
isa_t isa; // isa 是一個 union 聯合體,其包含這個對象所屬類的信息
public:
Class ISA(); // ISA() assumes this is NOT a tagged pointer object
Class getIsa(); // getIsa() allows this to be a tagged pointer object
...
};
struct objc_class : objc_object {
// 這里沒寫 isa,其實繼承了 objc_object 的 isa , 在這里 isa 是一個指向元類的指針
// Class ISA;
Class superclass; // 指向當前類的父類
cache_t cache; // formerly cache pointer and vtable
// 用于緩存指針和 vtable,加速方法的調用
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
// 相當于 class_rw_t 指針加上 rr/alloc 的標志
// bits 用于存儲類的方法、屬性、遵循的協議等信息的地方
// 針對 class_data_bits_t 的 data() 函數的封裝,最終返回一個 class_rw_t 類型的結構體變量
// Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 class_rw_t 中
class_rw_t *data() {
return bits.data();
}
...
};
objc_class 繼承于 objc_object,所以 objc_class 也是一個 objc_object,objc_object 和 objc_class 都有一個成員變量 isa。isa 變量的類型是 isa_t,這個 isa_t 其實是一個聯合體(union),其中包括成員量 cls。也就是說,每個 objc_object 通過自己持有的 isa,都可以查找到自己所屬的類,對于 objc_class 來說,就是通過 isa 找到自己所屬的元類(meta class)。
#define ISA_MASK 0x00007ffffffffff8ULL
#define ISA_MAGIC_MASK 0x001f800000000001ULL
#define ISA_MAGIC_VALUE 0x001d800000000001ULL
#define RC_ONE (1ULL<<56)
#define RC_HALF (1ULL<<7)
// isa 的類型是一個 isa_t 聯合體
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls; // 所屬的類
uintptr_t bits;
struct {
uintptr_t nonpointer : 1; // 表示 isa_t 的類型,0 表示 raw isa,也就是沒有結構體的部分,訪問對象的 isa 會直接返回一個指向 cls 的指針,也就是在 iPhone 遷移到 64 位系統之前時 isa 的類型。1 表示當前 isa 不是指針,但是其中也有 cls 的信息,只是其中關于類的指針都是保存在 shiftcls 中。
uintptr_t has_assoc : 1; // 對象含有或者曾經含有關聯引用,沒有關聯引用的可以更快地釋放內存
uintptr_t has_cxx_dtor : 1; // 表示當前對象有 C++ 或者 ObjC 的析構器(destructor),如果沒有析構器就會快速釋放內存。
uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t magic : 6; // 用于調試器判斷當前對象是真的對象還是沒有初始化的空間
uintptr_t weakly_referenced : 1; // 對象被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象可以更快釋放
uintptr_t deallocating : 1; // 對象正在釋放內存
uintptr_t has_sidetable_rc : 1; // 對象的引用計數太大了,存不下
uintptr_t extra_rc : 8; // 對象的引用計數超過 1,會存在這個這個里面,如果引用計數為 10,extra_rc 的值就為 9
};
};
而在 Objective-C 中,對象的方法都是存儲在類中,而不是對象中(如果每一個對象都保存了自己能執行的方法,那么對內存的占用有極大的影響)。
// Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 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;
...
};
當一個對象的實例方法被調用時,它要通過自己持有的 isa 來查找對應的類,然后在這里的 class_data_bits_t 結構體中查找對應方法的實現(每個對象可以通過 cls->data()-> methods
來訪問所屬類的方法)。同時,每一個 objc_class 也有一個指向自己的父類的指針 super_class 用來查找繼承的方法。
因為在 Objective-C 中,類其實也是一個對象,每個類也有一個 isa 指向自己所屬的元類。所以無論是類還是對象都能通過相同的機制查找方法的實現。
isa 在方法調用時扮演的角色:
- 調用一個對象的實例方法時,通過對象的 isa 在類中獲取方法的實現
- 調用一個類的類方法時,通過類的 isa 在元類中獲取方法的實現
- 如果在當前類/元類中沒找到,就會通過類/元類的 superclass 在繼承鏈中一級一級往上查找
<div align='center'>圖 1. 對象,類與元類之間的關系</div>
isa_t 中包含什么:
isa 的類型 isa_t 是一個 union 類型的結構體,也就是說其中的 isa_t、cls、 bits 還有結構體共用同一塊地址空間,而 isa 總共會占據 64 位的內存空間(決定于其中的結構體)。其中包含的信息見上面的代碼注釋。
現在直接訪問對象(objc_object)的 isa 已經不會返回類指針了,取而代之的是使用 ISA()
方法來獲取類指針。其中 ISA_MASK 是宏定義,這里通過掩碼的方式獲取類指針。
結論
(1)isa 的作用:用于查找對象(或類對象)所屬類(或元類)的信息,比如方法列表。
(2)isa 是什么:isa 的數據結構是一個 isa_t 聯合體,其中包含其所屬的 Class 的地址,通過訪問對象的 isa,就可以獲取到指向其所屬 Class 的指針(針對 tagged pointer 的情況,也就是 non-pointer isa,有點不一樣的是,除了指向 class 的指針,isa 中還會包含對象本身的一些信息,比如對象是否被弱引用)。
3. 為什么在 Objective-C 中,所以的對象都用一個指針來追蹤?
內存中的數據類型分為兩種:值類型和引用類型。指針就是引用類型,struct 類型就是值類型。
值類型在傳值時需要拷貝內容本身,而引用類型在傳遞時,拷貝的是對象的地址。所以,一方面,值類型的傳遞占用更多的內存空間,使用引用類型更節省內存開銷;另一方面,也是最主要的原因,很多時候,我們需要把一個對象交給另一個函數或者方法去修改其中的內容(比如說一個 Person 對象的 age 屬性),顯然如果我們想讓修改方獲取到這個對象,我們需要的傳遞的是地址,而不是復制一份。
對于像 int
這樣的基本數據類型,拷貝起來更快,而且數據簡單,方便修改,所以就不用指針了。
另一方面,對象的內存是分配在堆上的,而值類型是分配到棧上的。所以一般對象的生命周期會比普通的值類型要長,而且創建和銷毀對象以及內存管理是要消耗性能的,所以通過指針來引用一個對象,比直接復制和創建對象要更有效率、更節省性能。
參考:
- Understanding pointers?
- need of pointer objects in objective c
- Why "Everything" in Objective C is pointers. I mean why I should declare NSArray instance variables in pointers.
- Why do all objects in Objective-C have to use pointers?
4. Objective-C 對象是如何被創建(alloc)和初始化(init)的?
整個對象的創建過程其實就做了兩件事情:為對象分配內存空間,以及初始化 isa(一個聯合體)。
(1)創建 NSObject 對象的過程
+ (id)alloc {
return _objc_rootAlloc(self);
}
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
最核心的邏輯就在 _class_createInstanceFromZone
函數中:
static id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil) {
// 實例變量內存大小,實例大小 instanceSize 會存儲在類的 isa_t 結構體中,經過對齊最后返回
size_t size = cls->instanceSize(extraBytes);
// 給對象申請內存空間
id obj = (id)calloc(1, size);
if (!obj) return nil;
// 初始化 isa
obj->initInstanceIsa(cls, hasCxxDtor);
return obj;
}
獲取對象內存空間大?。?/p>
size_t instanceSize(size_t extraBytes) {
// Core Foundation 需要所有的對象的大小至少有 16 字節。
size_t size = alignedInstanceSize() + extraBytes;
if (size < 16) size = 16;
return size;
}
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize;
}
初始化 isa,isa 是一個 isa_t 聯合體:
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) {
if (!indexed) {
isa.cls = cls;
} else {
isa.bits = ISA_MAGIC_VALUE;
isa.has_cxx_dtor = hasCxxDtor;
isa.shiftcls = (uintptr_t)cls >> 3;
}
}
(2)NSObject 對象初始化的過程
NSObject 對象的初始化實際上就是返回 +alloc
執行后得到的對象本身:
- (id)init {
return _objc_rootInit(self);
}
id _objc_rootInit(id obj) {
return obj;
}
5. Objective-C 對象的實例變量是什么?為什么不能給 Objective-C 對象動態添加實例變量?
(1)兩個注意點:
- Objective-C 的
->
操作符不是C語言指針操作 - Objective-C 對象不能簡單對應于一個 C struct,訪問成員變量不等于訪問 C struct 成員
(2)Non Fragile ivars
在 Runtime 的現行版本中,最大的特點就是健壯的實例變量。
當一個類被編譯時,實例變量的布局也就形成了,它表明訪問類的實例變量的位置。用舊版OSX SDK 編譯的 MyObject 類成員變量布局是這樣的,MyObject的成員變量依次排列在基類NSObject 的成員后面。
當蘋果發布新版本OSX SDK后,NSObject增加了兩個成員變量。如果沒有Non Fragile ivars特性,我們的代碼將無法正常運行,因為MyObject類成員變量布局在編譯時已經確定,有兩個成員變量和基類的內存區域重疊了。此時,我們只能重新編譯MyObject代碼,程序才能在新版本系統上運行。
現在有了 Non Fragile ivars 之后,問題就解決了。在程序啟動后,runtime加載MyObject類的時候,通過計算基類的大小,runtime 動態調整了 MyObject 類成員變量布局,把MyObject成員變量的位置向后移動8個字節。于是我們的程序無需編譯,就能在新版本系統上運行。
(3)Non Fragile ivars 是如何實現的呢?
當成員變量布局調整后,靜態編譯的native程序怎么能找到變量的新偏移位置呢?
根據 Runtime 源碼可知,一個變量實際上就是一個 ivar_t 結構體。而每個 Objective-C 對象對應于 struct objc_object
,后者的 isa 指向類定義,即 struct objc_class:
typedef struct ivar_t *Ivar;
struct objc_object {
private:
isa_t isa;
//...
};
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 的 data()->ro->ivars
找下去,struct ivar_list_t是類所有成員變量的定義列表。通過 first
變量,可以取得類里任意一個類成員變量的定義。
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; // 一個指向常量的指針,其中存儲了當前類在編譯期就已經確定的屬性、方法以及遵循的協議
//...
};
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;
}
};
struct ivar_list_t {
uint32_t entsize;
uint32_t count;
ivar_t first;
};
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
//...
};
這里的 offset,應該就是用來記錄這個成員變量在對象中的偏移位置。也就是說,runtime在發現基類大小變化時,通過修改offset,來更新子類成員變量的偏移值。
假如我們現在有一個繼承于 NSError 的類 MyClass,在編譯時,LLVM計算出基類 NSError 對象的大小為40字節,然后記錄在MyClass的類定義中,如下是對應的C代碼。在編譯后的可執行程序中,寫死了“40”這個魔術數字,記錄了在此次編譯時MyClass基類的大小。
class_ro_t class_ro_MyClass = {
.instanceStart = 40,
.instanceSize = 48,
//...
}
現在假如蘋果發布了OSX 11 SDK,NSError類大小增加到48字節。當我們的程序啟動后,runtime加載MyClass類定義的時候,發現基類的真實大小和MyClass的instanceStart不相符,得知基類的大小發生了改變。于是runtime遍歷MyClass的所有成員變量定義,將offset指向的值增加8。具體的實現代碼在runtime/objc-runtime-new.mm的 moveIvars()
函數中。
并且,MyClass類定義的instanceSize也要增加8。這樣runtime在創建MyClass對象的時候,能分配出正確大小的內存塊。
static void moveIvars(class_ro_t *ro, uint32_t superSize)
{
uint32_t diff;
diff = superSize - ro->instanceStart;
if (ro->ivars) {
// Find maximum alignment in this class's ivars
uint32_t maxAlignment = 1;
for (const auto& ivar : *ro->ivars) {
if (!ivar.offset) continue; // anonymous bitfield
uint32_t alignment = ivar.alignment();
if (alignment > maxAlignment) maxAlignment = alignment;
}
// Compute a slide value that preserves that alignment
uint32_t alignMask = maxAlignment - 1;
diff = (diff + alignMask) & ~alignMask;
// Slide all of this class's ivars en masse
for (const auto& ivar : *ro->ivars) {
if (!ivar.offset) continue; // anonymous bitfield
uint32_t oldOffset = (uint32_t)*ivar.offset;
uint32_t newOffset = oldOffset + diff;
*ivar.offset = newOffset;
if (PrintIvars) {
_objc_inform("IVARS: offset %u -> %u for %s "
"(size %u, align %u)",
oldOffset, newOffset, ivar.name,
ivar.size, ivar.alignment());
}
}
}
*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;
}
(4)為什么Objective-C類不能動態添加成員變量
runtime 提供了一個 class_addIvar() 函數用于給類添加成員變量,但是根據文檔中的注釋,這個函數只能在“構建一個類的過程中”調用。一旦完成類定義,就不能再添加成員變量了。經過編譯的類在程序啟動后就被runtime加載,沒有機會調用addIvar。程序在運行時動態構建的類需要在調用 objc_allocateClassPair 之后,調用 objc_registerClassPair 之前才可以添加成員變量。
這樣做會帶來嚴重問題,為基類動態增加成員變量會導致所有已創建出的子類實例都無法使用。那為什么runtime允許動態添加方法和屬性,而不會引發問題呢?
因為方法和屬性并不“屬于”實例,而成員變量“屬于”實例。我們所說的“類實例”概念,指的是一塊內存區域,包含了isa指針和所有的成員變量。所以假如允許動態修改類成員變量布局,已經創建出的實例就不符合類定義了,變成了無效對象。但方法定義是在objc_class中管理的,不管如何增刪類方法,都不影響實例的內存布局,已經創建出的實例仍然可正常使用。
美團的技術博客中給出的解釋比較簡單,其中“破壞破壞類的內部布局”這句話本身也是有些問題的:
extension在編譯期決議,它就是類的一部分,在編譯期和頭文件里的@interface以及實現文件里的@implement一起形成一個完整的類,它伴隨類的產生而產生,亦隨之一起消亡。extension一般用來隱藏類的私有信息,你必須有一個類的源碼才能為一個類添加extension,所以你無法為系統的類比如NSString添加extension。
但是category則完全不一樣,它是在運行期決議的。
就category和extension的區別來看,我們可以推導出一個明顯的事實,extension可以添加實例變量,而category是無法添加實例變量的(因為在運行期,對象的內存布局已經確定,如果添加實例變量就會破壞類的內部布局,這對編譯型語言來說是災難性的)。
參考:
6. Objective-C 對象的屬性是什么?屬性跟實例變量的區別?
屬性是一個結構體,其中包含屬性名和屬性本身的屬性(attributes)。
我們一般是通過使用 @property
進行屬性定義,編譯時編譯器會自動生成對應的實例變量(默認情況下生成的實例變量名是在對應的屬性名前加了下劃線“_”),同時還會自動合成對應的 setter 和 getter 方法用于存取屬性值。
我們可以驗證一下,先定義一個帶有屬性的類 NyanCat,如下:
@interface NyanCat : NSObject {
int age;
NSString *name;
}
@property (nonatomic, copy) NSString *cost;
@end
@implementation NyanCat
@end
然后再通過 clang -rewrite-objc NyanCat.m
將該類重寫為 cpp 代碼后,得到了下面這些內容:
#ifndef _REWRITER_typedef_NyanCat
#define _REWRITER_typedef_NyanCat
typedef struct objc_object NyanCat; // NyanCat 類實際上就是一個 objc_object 結構體
typedef struct {} _objc_exc_NyanCat;
#endif
extern "C" unsigned long OBJC_IVAR_$_NyanCat$_cost;
struct NyanCat_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int age;
NSString *name;
NSString *_cost;
};
//...
// 屬性 cost 的 setter 和 getter 對應的函數
static NSString * _I_NyanCat_cost(NyanCat * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_NyanCat$_cost)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_NyanCat_setCost_(NyanCat * self, SEL _cmd, NSString *cost) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct NyanCat, _cost), (id)cost, 0, 1); }
// 屬性的數據結構
struct _prop_t {
const char *name;
const char *attributes;
};
extern "C" unsigned long int OBJC_IVAR_$_NyanCat$age __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct NyanCat, age);
extern "C" unsigned long int OBJC_IVAR_$_NyanCat$name __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct NyanCat, name);
extern "C" unsigned long int OBJC_IVAR_$_NyanCat$_cost __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct NyanCat, _cost);
// 實例變量列表
static struct /*_ivar_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count;
struct _ivar_t ivar_list[3];
} _OBJC_$_INSTANCE_VARIABLES_NyanCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_ivar_t),
3,
{{(unsigned long int *)&OBJC_IVAR_$_NyanCat$age, "age", "i", 2, 4},
{(unsigned long int *)&OBJC_IVAR_$_NyanCat$name, "name", "@\"NSString\"", 3, 8},
{(unsigned long int *)&OBJC_IVAR_$_NyanCat$_cost, "_cost", "@\"NSString\"", 3, 8}}
};
// 實例方法列表
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_INSTANCE_METHODS_NyanCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{(struct objc_selector *)"cost", "@16@0:8", (void *)_I_NyanCat_cost},
{(struct objc_selector *)"setCost:", "v24@0:8@16", (void *)_I_NyanCat_setCost_}}
};
// 屬性列表
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_NyanCat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"cost","T@\"NSString\",C,N,V_cost"}}
};
從上面 clang 重寫的代碼中可以看到:
- 屬性列表的數據結構 _prop_list_t 中有屬性
cost
對應的屬性名和屬性的 attributes(attributes 字符串所代表的含義可以在官方文檔上查閱到)。 - 實例變量列表
_method_list_t
中也有屬性cost
對應的變量信息,變量名為_cost
,類型為@"NSString"
。 - 實例方法列表
_method_list_t
中有屬性cost
對應的 setter 和 getter 方法,這兩個方法的實現分別對應的是兩個函數——_I_NyanCat_setCost_(NyanCat * self, SEL _cmd, NSString *cost)
和_I_NyanCat_cost(NyanCat * self, SEL _cmd)
。
以上三條結果正好驗證了我們一開始提出的結論。
實際上,在 runtime 源碼 objc-runtime-new.h 的實現中,屬性就是一個 property_t 類型的結構體,其中包含屬性名以及屬性自己的屬性(attributes)。
typedef struct property_t *objc_property_t;
// 屬性的數據結構
struct property_t {
const char *name; // property 的名字
const char *attributes; // property 的屬性
};
在實際使用 runtime 時,通過下面兩個函數分別可以獲取 property 名字和 attributes 字符串。
// Returns the name of a property.
const char * _Nonnull property_getName(objc_property_t _Nonnull property);
// Returns the attribute string of a property.
const char * _Nullable property_getAttributes(objc_property_t _Nonnull property);
小結:
- 一個對象的屬性實際上包括實例變量以及存取屬性值(實際上就是實例變量值)的 setter/getter 方法兩部分(這里只討論類本身定義的屬性,category 中的 @property 并沒有為我們生成實例變量以及存取方法,而需要我們手動實現)。
- 屬性的實際結構是一個結構體,其中包含屬性名和 attributes 兩部分。
7. Objective-C 對象的方法是什么?Objective-C 對象的方法在內存中的存儲結構是什么樣的?
objc_class 有一個 class_data_bits_t
類型的變量 bits,Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 class_rw_t 中,通過調用 objc_class 的 class_rw_t *data() 方法,可以獲取這個 class_rw_t 類型的變量。
// Objective-C 類是一個結構體,繼承于 objc_object
struct objc_class : objc_object {
// 這里沒寫 isa,其實繼承了 objc_object 的 isa , 在這里 isa 是一個指向元類的指針
// Class ISA;
Class superclass; // 指向當前類的父類
cache_t cache; // formerly cache pointer and vtable
// 用于緩存指針和 vtable,加速方法的調用
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
// 相當于 class_rw_t 指針加上 rr/alloc 的標志
// bits 用于存儲類的方法、屬性、遵循的協議等信息的地方
// 針對 class_data_bits_t 的 data() 函數的封裝,最終返回一個 class_rw_t 類型的結構體變量
// Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 class_rw_t 中
class_rw_t *data() {
return bits.data();
}
...
}
class_rw_t 中還有一個指向常量的指針 ro,其中存儲了當前類在編譯期就已經確定的屬性、方法以及遵循的協議。
/ Objective-C 類中的屬性、方法還有遵循的協議等信息都保存在 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; // 所遵循的協議的列表
...
}
// 用于存儲一個 Objective-C 類在編譯期就已經確定的屬性、方法以及遵循的協議
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;
}
};
加載 ObjC 運行時的過程中在 realizeClass()
方法中:
- 從 class_data_bits_t 調用 data 方法,將結果強制轉換為 class_ro_t 指針;
- 初始化一個 class_rw_t 結構體;
- 設置結構體 ro 的值以及 flag。
- 最后重新將這個 class_rw_t 設置給 class_data_bits_t 的 data。
...
const class_ro_t *ro = (const class_ro_t *)cls->data();
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
...
在上面這段代碼運行之后 class_rw_t
中的方法,屬性以及協議列表均為空。這時需要 realizeClass
調用 methodizeClass
方法來將類自己實現的方法(包括分類)、屬性和遵循的協議加載到 methods、 properties 和 protocols 列表中。
方法的結構,與類和對象一樣,方法在內存中也是一個結構體 method_t,其中包括成員變量 name(SEL 類型,實際上就是方法名)、types(一個C字符串方法類型,詳見 Type Encodings)、imp(IMP 類型方法實現)。
struct method_t {
SEL name;
const char *types;
IMP imp;
};
結論:
(1) 在 runtime 初始化之后,realizeClass 之前,從 class_data_bits_t 結構體中獲取的 class_rw_t 一直都不是 class_rw_t 結構體,而是class_ro_t。因為類的一些方法、屬性和協議都是在編譯期決定的(baseMethods 等成員以及類在內存中的位置都是編譯期決定的)。
(2) 類在內存中的位置是在編譯期間決定的,在之后修改代碼,也不會改變內存中的位置。
類的方法、屬性以及協議在編譯期間存放到了“錯誤”的位置,直到 realizeClass 執行之后,才放到了 class_rw_t 指向的只讀區域 class_ro_t,這樣我們即可以在運行時為 class_rw_t 添加方法,也不會影響類的只讀結構。
(3) 在 class_ro_t 中的屬性在運行期間就不能改變了,再添加方法時,會修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods。
(4)一個類(Class)持有一個分發表,在運行期分發消息,表中的每一個實體代表一個方法(Method),它的名字叫做選擇子(SEL),對應著一種方法實現(IMP)。
參考:
8. 什么是 IMP?什么是選擇器 selector ?
8.1 IMP
IMP 在 runtime 源碼 objc.h 中的定義是:
/// A pointer to the function of a method implementation.
typedef void (*IMP)(void /* id, SEL, ... */ );
它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息之后,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法的實現,以達到更好的性能(在 Mantle 的 MTLModelAdapter.m
中可以看到這方面的應用)。
你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含 id 和 SEL 類型。每個方法名都對應一個 SEL 類型的方法選擇器,而每個實例對象中的 SEL 對應的方法實現肯定是唯一的,通過一組 id 和 SEL 參數就能確定唯一的方法實現地址;反之亦然。
8.2 選擇器 selector
選擇器代表方法在 Runtime 期間的標識符,為 SEL 類型,SEL 與普通字符串的區別在于 SEL 對于選擇器來說總是能保證其唯一性。在類加載的時候,編譯器會生成與方法相對應的選擇子,并注冊到 Objective-C 的 Runtime 運行系統。
SEL 在 objc.h 中的定義是:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
SEL 看上去是一個指向結構體的指針,但是實際上是什么類型呢?objc.h 中提供了運行時向系統注冊選擇器的函數 sel_registerName()
。而在開源的 objc-sel.mm
中提供了sel_registerName()
函數的實現,其中能找到一些蛛絲馬跡:
SEL sel_registerName(const char *name) {
return __sel_registerName(name, 1, 1); // YES lock, YES copy
}
static SEL __sel_registerName(const char *name, int lock, int copy)
{
SEL result = 0;
if (lock) selLock.assertUnlocked();
else selLock.assertWriting();
// name 為空直接返回 0
if (!name) return (SEL)0;
result = search_builtins(name);
if (result) return result;
if (lock) selLock.read();
if (namedSelectors) {
// 到全局的表中去找
result = (SEL)NXMapGet(namedSelectors, name);
}
if (lock) selLock.unlockRead();
if (result) return result;
// No match. Insert.
if (lock) selLock.write();
if (!namedSelectors) {
namedSelectors = NXCreateMapTable(NXStrValueMapPrototype,
(unsigned)SelrefCount);
}
if (lock) {
// Rescan in case it was added while we dropped the lock
result = (SEL)NXMapGet(namedSelectors, name);
}
if (!result) {
// 創建一個 SEL
result = sel_alloc(name, copy);
// fixme choose a better container (hash not map for starters)
NXMapInsert(namedSelectors, sel_getName(result), result);
}
if (lock) selLock.unlockWrite();
return result;
}
static SEL sel_alloc(const char *name, bool copy)
{
selLock.assertWriting();
return (SEL)(copy ? strdupIfMutable(name) : name);
}
從創建 SEL 的實現來看, SEL 實際上是一個 char *
類型,也就是一個字符串。
(1) 使用 @selector()
生成的選擇子不會因為類的不同而改變(即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇子),其內存地址在編譯期間就已經確定了。也就是說向不同的類發送相同的消息時,其生成的選擇子是完全相同的。
(2) 通過 @selector(方法名)
就可以返回一個選擇子,通過 (void *)@selector(方法名)
, 就可以讀取選擇器的地址。
(3) 推斷出的 selector 的特性:
- Objective-C 為我們維護了一個巨大的選擇子表
- 在使用
@selector()
時會從這個選擇子表中根據選擇子的名字查找對應的 SEL。如果沒有找到,則會生成一個SEL
并添加到表中。 - 在編譯期間會掃描全部的頭文件和實現文件將其中的方法以及使用
@selector()
生成的選擇子加入到選擇子表中。
參考:
9. 消息發送和消息轉發
具體過程查看源碼中
lookUpImpOrForward()
函數部分的注釋
- 發送 hello 消息后,編譯器會將上面這行 [obj hello]; 代碼轉成 objc_msgSend()(注:objc_msgSend 是一個私有方法,而且是用匯編實現的,我們沒有辦法進入它的實現,但是我們可以通過 lookUpImpOrForward 函數斷點攔截)
- 到當前類的緩存中去查找方法實現,如果找到了直接 done
- 如果沒找到,就到當前類的方法列表中去查找,如果找到了直接 done
- 如果還沒找到,就到父類的緩存中去查找方法實現,如果找到了直接 done
- 如果沒找到,就到父類的方法列表中去查找,如果找到了直接 done
- 如果還沒找到,就進行方法決議
- 最后還沒找到的話,就走消息轉發
參考:
10. Method Swizzling
- 什么是 Method Swizzling ?
- Method Swizzling 有什么注意點?
- Method Swizzling 的原理是什么?
- Method Swizzling 為什么要在 +load 方法中進行?
11. Category
- Category 是什么?
- Category 中的方法和屬性以及協議是怎么存儲和加載的?
- Category 和 Class 的關系
12. Associated Objects 的原理是什么?到底能不能在 Category 中給 Objective-C 類添加屬性和實例變量?
- Associated Objects 的原理是什么?
- Associated Objects 的內存管理機制?
- 到底能不能在 Category 中給 Objective-C 類添加屬性和實例變量?
13. Objective-C 中的 Protocol 是什么?
14. self
和 super
的本質
self 和 super 兩個是不同的,self 是方法的一個隱藏參數(每個方法都有兩個隱藏的參數,self
和 _cmd
),每個方法的實現的第一個參數即為 self。而 super 不是一個隱藏參數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當調用 [super xxx]方法時,去調用父類的方法,而不是本類中的方法。
我們可以看看 message.h 中提供的發消息給父類的函數:
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ );
當我們發送消息給 super 時,runtime 時就不是使用 objc_msgSend 方法了,而是 objc_msgSendSuper。函數的第一個參數也不再是 self 了,編譯器會生成一個 objc_super 結構體。下面是 message.h 中 objc_super 結構體的定義:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
objc_super 包含了兩個變量,receiver 是消息的實際接收者,super_class 是指向當前類的父類。
通過 clang -rewrite-objc NyanCat.m
命令將下面定義的 NyanCat 類轉成 cpp 代碼。
#import <Foundation/Foundation.h>
@interface NyanCat : NSObject
@end
@implementation NyanCat
- (instancetype)init {
self = [super init];
return self;
}
@end
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
#ifndef _REWRITER_typedef_NyanCat
#define _REWRITER_typedef_NyanCat
typedef struct objc_object NyanCat;
typedef struct {} _objc_exc_NyanCat;
#endif
struct NyanCat_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
/* @end */
// @implementation NyanCat
static instancetype _I_NyanCat_init(NyanCat * self, SEL _cmd) {
self = ((NyanCat *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("NyanCat"))}, sel_registerName("init"));
return self;
}
// ...
由此可見,[super xxxx]
在運行時確實被轉成了 objc_msgSendSuper 函數。
那么 objc_msgSendSuper 這個函數的內部實現是怎么樣的呢?文檔 objc_msgSendSuper 函數的注釋中對 super 參數的注釋是這樣寫的:
super - A pointer to an objc_super data structure. Pass values identifying the context the message was sent to, including the instance of the class that is to receive the message and the superclass at which to start searching for the method implementation.
我們可以推斷出,objc_msgSendSuper 函數實現實際上就是:從 objc_super
結構體指向的 objc_super->superClass
的方法列表開始查找調用方法的 selector 對應的實現,找到后以 objc_super->receiver
去調用這個 selector,最后就變成了調用 objc_msgSend 函數給 self 發消息的形式了。
objc_msgSend(objc_super->receiver, @selector(xxx));
這里的 objc_super->receiver 就相當于 self,上面的操作其實就是:
objc_msgSend(self, @selector(xxx));
[self init]
和 [super init]
的相同點在于消息接收者實際上都是 self(方法調用源頭),區別就在于查找方法的實現時,前者是從 currentClass(self 所屬的類)的方法列表中開始往上找,而后者是從 objc_super->spuerClass(也就是調用了 super 的地方的父類,這是在編譯時就確定了的)的方法列表中開始往上查找。
需要強調的地方是,[self xxx]
要調用的實現是在運行時動態決定的,而 [super xxx]
要調用的實現是編譯時就確定了的(這里有個例子可以測試一下)。從上面轉換出來的 cpp 代碼中也可以看出來,這其實是因為 objc_msgSendSuper
函數的第一個參數 objc_super
結構體中的 receiver
是通過接收方法中的 self 參數得來的,所以動態決定的,而 objc_super->superClass
是通過 class_getSuperclass(objc_getClass("NyanCat"))
得到的,所以是靜態的,在編譯時就確定了的。
static instancetype _I_NyanCat_init(NyanCat * self, SEL _cmd) {
self = ((NyanCat *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("NyanCat"))}, sel_registerName("init"));
return self;
}
參考:
- Objc Runtime
- Objective C: Difference between self and super
- What does it mean when you assign [super init] to self?
15. load 方法和 initialize 方法
-
+load
方法和+initialize
方法分別在什么時候被調用? - 這兩個方法是用來干嘛的?
- ProtocolKit 的實現中為什么要在 main 函數執行前進行 Protocol 方法默認實現的注冊?
延伸
- clang 命令的使用(比如
clang -rewrite-objc test.m
),clang -rewrite-objc
的作用是什么?clang rewrite 出來的文件跟 objc runtime 源碼的實現有什么區別嗎?
參考:
- Understanding the Objective-C Runtime
- Objective-C Runtime - 玉令天下的博客
- Objective-C 中的類和對象 - ibireme 的博客
- Draveness 出品的 runtime 源碼閱讀系列文章(強烈推薦)
- 對象是如何初始化的(iOS):介紹了 Objective-C 對象初始化的過程
- 從 NSObject 的初始化了解 isa:深入剖析了 isa 的結構和作用
- 深入解析 ObjC 中方法的結構:介紹了在 ObjC 中是如何存儲方法的
- 從源代碼看 ObjC 中消息的發送 :通過逐步斷點調試 objc 源碼的方式,從 Objc 源代碼中分析并合理地推測一些關于消息傳遞的過程
- 從 ObjC Runtime 源碼分析一個對象創建的過程
- Objective-C 對象模型 - 雷純鋒的技術博客
- Objc 對象的今生今世
- Runtime源碼 —— 概述和調試環境準備:作者寫了一個系列的文章,內容很詳細
- Objective-C runtime - 系列開始:簡單介紹了學習 objc 源代碼的方法