
前言
我第一次開始重視Objective-C Runtime是從2014年11月1日,@唐巧老師在微博上發(fā)的一條微博開始。

這是sunnyxx在線下的一次分享會(huì)。會(huì)上還給了4道題目。

這4道題以我當(dāng)時(shí)的知識(shí),很多就不確定,拿不準(zhǔn)。從這次入院考試開始,就成功入院了。后來這兩年對(duì)Runtime的理解慢慢增加了,打算今天自己總結(jié)總結(jié)平時(shí)一直躺在我印象筆記里面的筆記。有些人可能有疑惑,學(xué)習(xí)Runtime到底有啥用,平時(shí)好像并不會(huì)用到。希望看完我這次的總結(jié),心中能解開一些疑惑。
目錄
- 1.Runtime簡介
- 2.NSObject起源
- (1) isa_t結(jié)構(gòu)體的具體實(shí)現(xiàn)
- (2) cache_t的具體實(shí)現(xiàn)
- (3) class_data_bits_t的具體實(shí)現(xiàn)
- 3.入院考試
一. Runtime簡介
Runtime 又叫運(yùn)行時(shí),是一套底層的 C 語言 API,是 iOS 系統(tǒng)的核心之一。開發(fā)者在編碼過程中,可以給任意一個(gè)對(duì)象發(fā)送消息,在編譯階段只是確定了要向接收者發(fā)送這條消息,而接受者將要如何響應(yīng)和處理這條消息,那就要看運(yùn)行時(shí)來決定了。
C語言中,在編譯期,函數(shù)的調(diào)用就會(huì)決定調(diào)用哪個(gè)函數(shù)。
而OC的函數(shù),屬于動(dòng)態(tài)調(diào)用過程,在編譯期并不能決定真正調(diào)用哪個(gè)函數(shù),只有在真正運(yùn)行時(shí)才會(huì)根據(jù)函數(shù)的名稱找到對(duì)應(yīng)的函數(shù)來調(diào)用。
Objective-C 是一個(gè)動(dòng)態(tài)語言,這意味著它不僅需要一個(gè)編譯器,也需要一個(gè)運(yùn)行時(shí)系統(tǒng)來動(dòng)態(tài)得創(chuàng)建類和對(duì)象、進(jìn)行消息傳遞和轉(zhuǎn)發(fā)。
Objc 在三種層面上與 Runtime 系統(tǒng)進(jìn)行交互:

1. 通過 Objective-C 源代碼
一般情況開發(fā)者只需要編寫 OC 代碼即可,Runtime 系統(tǒng)自動(dòng)在幕后把我們寫的源代碼在編譯階段轉(zhuǎn)換成運(yùn)行時(shí)代碼,在運(yùn)行時(shí)確定對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)和調(diào)用具體哪個(gè)方法。
2. 通過 Foundation 框架的 NSObject 類定義的方法
在OC的世界中,除了NSProxy類以外,所有的類都是NSObject的子類。在Foundation框架下,NSObject和NSProxy兩個(gè)基類,定義了類層次結(jié)構(gòu)中該類下方所有類的公共接口和行為。NSProxy是專門用于實(shí)現(xiàn)代理對(duì)象的類,這個(gè)類暫時(shí)本篇文章不提。這兩個(gè)類都遵循了NSObject協(xié)議。在NSObject協(xié)議中,聲明了所有OC對(duì)象的公共方法。
在NSObject協(xié)議中,有以下5個(gè)方法,是可以從Runtime中獲取信息,讓對(duì)象進(jìn)行自我檢查。
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (BOOL)respondsToSelector:(SEL)aSelector;
-class方法返回對(duì)象的類;
-isKindOfClass: 和 -isMemberOfClass: 方法檢查對(duì)象是否存在于指定的類的繼承體系中(是否是其子類或者父類或者當(dāng)前類的成員變量);
-respondsToSelector: 檢查對(duì)象能否響應(yīng)指定的消息;
-conformsToProtocol:檢查對(duì)象是否實(shí)現(xiàn)了指定協(xié)議類的方法;
在NSObject的類中還定義了一個(gè)方法
- (IMP)methodForSelector:(SEL)aSelector;
這個(gè)方法會(huì)返回指定方法實(shí)現(xiàn)的地址IMP。
以上這些方法會(huì)在本篇文章中詳細(xì)分析具體實(shí)現(xiàn)。
3. 通過對(duì) Runtime 庫函數(shù)的直接調(diào)用
關(guān)于庫函數(shù)可以在Objective-C Runtime Reference中查看 Runtime 函數(shù)的詳細(xì)文檔。
關(guān)于這一點(diǎn),其實(shí)還有一個(gè)小插曲。當(dāng)我們導(dǎo)入了objc/Runtime.h和objc/message.h兩個(gè)頭文件之后,我們查找到了Runtime的函數(shù)之后,代碼打完,發(fā)現(xiàn)沒有代碼提示了,那些函數(shù)里面的參數(shù)和描述都沒有了。對(duì)于熟悉Runtime的開發(fā)者來說,這并沒有什么難的,因?yàn)閰?shù)早已銘記于胸。但是對(duì)于新手來說,這是相當(dāng)不友好的。而且,如果是從iOS6開始開發(fā)的同學(xué),依稀可能能感受到,關(guān)于Runtime的具體實(shí)現(xiàn)的官方文檔越來越少了?可能還懷疑是不是錯(cuò)覺。其實(shí)從Xcode5開始,蘋果就不建議我們手動(dòng)調(diào)用Runtime的API,也同樣希望我們不要知道具體底層實(shí)現(xiàn)。所以IDE上面默認(rèn)代了一個(gè)參數(shù),禁止了Runtime的代碼提示,源碼和文檔方面也刪除了一些解釋。
具體設(shè)置如下:

如果發(fā)現(xiàn)導(dǎo)入了兩個(gè)庫文件之后,仍然沒有代碼提示,就需要把這里的設(shè)置改成NO,即可。
二. NSObject起源
由上面一章節(jié),我們知道了與Runtime交互有3種方式,前兩種方式都與NSObject有關(guān),那我們就從NSObject基類開始說起。

以下源碼分析均來自objc4-680
NSObject的定義如下
typedef struct objc_class *Class;
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
在Objc2.0之前,objc_class源碼如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在這里可以看到,在一個(gè)類中,有超類的指針,類名,版本的信息。
ivars是objc_ivar_list成員變量列表的指針;methodLists是指向objc_method_list指針的指針。*methodLists是指向方法列表的指針。這里如果動(dòng)態(tài)修改*methodLists的值來添加成員方法,這也是Category實(shí)現(xiàn)的原理,同樣解釋了Category不能添加屬性的原因。
關(guān)于Category,這里推薦2篇文章可以仔細(xì)研讀一下。
深入理解Objective-C:Category
結(jié)合 Category 工作原理分析 OC2.0 中的 runtime
然后在2006年蘋果發(fā)布Objc 2.0之后,objc_class的定義就變成下面這個(gè)樣子了。
typedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
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
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}

把源碼的定義轉(zhuǎn)化成類圖,就是上圖的樣子。
從上述源碼中,我們可以看到,Objective-C 對(duì)象都是 C 語言結(jié)構(gòu)體實(shí)現(xiàn)的,在objc2.0中,所有的對(duì)象都會(huì)包含一個(gè)isa_t類型的結(jié)構(gòu)體。
objc_object被源碼typedef成了id類型,這也就是我們平時(shí)遇到的id類型。這個(gè)結(jié)構(gòu)體中就只包含了一個(gè)isa_t類型的結(jié)構(gòu)體。這個(gè)結(jié)構(gòu)體在下面會(huì)詳細(xì)分析。
objc_class繼承于objc_object。所以在objc_class中也會(huì)包含isa_t類型的結(jié)構(gòu)體isa。至此,可以得出結(jié)論:Objective-C 中類也是一個(gè)對(duì)象。在objc_class中,除了isa之外,還有3個(gè)成員變量,一個(gè)是父類的指針,一個(gè)是方法緩存,最后一個(gè)這個(gè)類的實(shí)例方法鏈表。
object類和NSObject類里面分別都包含一個(gè)objc_class類型的isa。
上圖的左半邊類的關(guān)系描述完了,接著先從isa來說起。
當(dāng)一個(gè)對(duì)象的實(shí)例方法被調(diào)用的時(shí)候,會(huì)通過isa找到相應(yīng)的類,然后在該類的class_data_bits_t中去查找方法。class_data_bits_t是指向了類對(duì)象的數(shù)據(jù)區(qū)域。在該數(shù)據(jù)區(qū)域內(nèi)查找相應(yīng)方法的對(duì)應(yīng)實(shí)現(xiàn)。
但是在我們調(diào)用類方法的時(shí)候,類對(duì)象的isa里面是什么呢?這里為了和對(duì)象查找方法的機(jī)制一致,遂引入了元類(meta-class)的概念。
關(guān)于元類,更多具體可以研究這篇文章What is a meta-class in Objective-C?
在引入元類之后,類對(duì)象和對(duì)象查找方法的機(jī)制就完全統(tǒng)一了。
對(duì)象的實(shí)例方法調(diào)用時(shí),通過對(duì)象的 isa 在類中獲取方法的實(shí)現(xiàn)。
類對(duì)象的類方法調(diào)用時(shí),通過類的 isa 在元類中獲取方法的實(shí)現(xiàn)。
meta-class之所以重要,是因?yàn)樗鎯?chǔ)著一個(gè)類的所有類方法。每個(gè)類都會(huì)有一個(gè)單獨(dú)的meta-class,因?yàn)槊總€(gè)類的類方法基本不可能完全相同。
對(duì)應(yīng)關(guān)系的圖如下圖,下圖很好的描述了對(duì)象,類,元類之間的關(guān)系:

圖中實(shí)線是 super_class指針,虛線是isa指針。
- Root class (class)其實(shí)就是NSObject,NSObject是沒有超類的,所以Root class(class)的superclass指向nil。
- 每個(gè)Class都有一個(gè)isa指針指向唯一的Meta class
- Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個(gè)回路。
- 每個(gè)Meta class的isa指針都指向Root class (meta)。
我們其實(shí)應(yīng)該明白,類對(duì)象和元類對(duì)象是唯一的,對(duì)象是可以在運(yùn)行時(shí)創(chuàng)建無數(shù)個(gè)的。而在main方法執(zhí)行之前,從 dyld到runtime這期間,類對(duì)象和元類對(duì)象在這期間被創(chuàng)建。具體可看sunnyxx這篇iOS 程序 main 函數(shù)之前發(fā)生了什么
(1)isa_t結(jié)構(gòu)體的具體實(shí)現(xiàn)
接下來我們就該研究研究isa的具體實(shí)現(xiàn)了。objc_object里面的isa是isa_t類型。通過查看源碼,我們可以知道isa_t是一個(gè)union聯(lián)合體。
struct objc_object {
private:
isa_t isa;
public:
// initIsa() should be used to init the isa of new objects only.
// If this object already has an isa, use changeIsa() for correctness.
// initInstanceIsa(): objects with no custom RR/AWZ
void initIsa(Class cls /*indexed=false*/);
void initInstanceIsa(Class cls, bool hasCxxDtor);
private:
void initIsa(Class newCls, bool indexed, bool hasCxxDtor);
}
那就從initIsa方法開始研究。下面以arm64為例。
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
initIsa(cls, true, hasCxxDtor);
}
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;
}
}
initIsa第二個(gè)參數(shù)傳入了一個(gè)true,所以initIsa就會(huì)執(zhí)行else里面的語句。
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
uintptr_t indexed : 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;
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)
};

ISA_MAGIC_VALUE = 0x000001a000000001ULL轉(zhuǎn)換成二進(jìn)制是11010000000000000000000000000000000000001,結(jié)構(gòu)如下圖:

關(guān)于參數(shù)的說明:
第一位index,代表是否開啟isa指針優(yōu)化。index = 1,代表開啟isa指針優(yōu)化。
在2013年9月,蘋果推出了iPhone5s,與此同時(shí),iPhone5s配備了首個(gè)采用64位架構(gòu)的A7雙核處理器,為了節(jié)省內(nèi)存和提高執(zhí)行效率,蘋果提出了Tagged Pointer的概念。對(duì)于64位程序,引入Tagged Pointer后,相關(guān)邏輯能減少一半的內(nèi)存占用,以及3倍的訪問速度提升,100倍的創(chuàng)建、銷毀速度提升。
在WWDC2013的《Session 404 Advanced in Objective-C》視頻中,蘋果介紹了 Tagged Pointer。 Tagged Pointer的存在主要是為了節(jié)省內(nèi)存。我們知道,對(duì)象的指針大小一般是與機(jī)器字長有關(guān),在32位系統(tǒng)中,一個(gè)指針的大小是32位(4字節(jié)),而在64位系統(tǒng)中,一個(gè)指針的大小將是64位(8字節(jié))。
假設(shè)我們要存儲(chǔ)一個(gè)NSNumber對(duì)象,其值是一個(gè)整數(shù)。正常情況下,如果這個(gè)整數(shù)只是一個(gè)NSInteger的普通變量,那么它所占用的內(nèi)存是與CPU的位數(shù)有關(guān),在32位CPU下占4個(gè)字節(jié),在64位CPU下是占8個(gè)字節(jié)的。而指針類型的大小通常也是與CPU位數(shù)相關(guān),一個(gè)指針?biāo)加玫膬?nèi)存在32位CPU下為4個(gè)字節(jié),在64位CPU下也是8個(gè)字節(jié)。如果沒有Tagged Pointer對(duì)象,從32位機(jī)器遷移到64位機(jī)器中后,雖然邏輯沒有任何變化,但這種NSNumber、NSDate一類的對(duì)象所占用的內(nèi)存會(huì)翻倍。如下圖所示:

蘋果提出了Tagged Pointer對(duì)象。由于NSNumber、NSDate一類的變量本身的值需要占用的內(nèi)存大小常常不需要8個(gè)字節(jié),拿整數(shù)來說,4個(gè)字節(jié)所能表示的有符號(hào)整數(shù)就可以達(dá)到20多億(注:2^31=2147483648,另外1位作為符號(hào)位),對(duì)于絕大多數(shù)情況都是可以處理的。所以,引入了Tagged Pointer對(duì)象之后,64位CPU下NSNumber的內(nèi)存圖變成了以下這樣:

關(guān)于Tagged Pointer技術(shù)詳細(xì)的,可以看上面鏈接那個(gè)文章。
has_assoc
對(duì)象含有或者曾經(jīng)含有關(guān)聯(lián)引用,沒有關(guān)聯(lián)引用的可以更快地釋放內(nèi)存
has_cxx_dtor
表示該對(duì)象是否有 C++ 或者 Objc 的析構(gòu)器
shiftcls
類的指針。arm64架構(gòu)中有33位可以存儲(chǔ)類指針。
源碼中isa.shiftcls = (uintptr_t)cls >> 3;
將當(dāng)前地址右移三位的主要原因是用于將 Class 指針中無用的后三位清除減小內(nèi)存的消耗,因?yàn)轭惖闹羔樢凑兆止?jié)(8 bits)對(duì)齊內(nèi)存,其指針后三位都是沒有意義的 0。具體可以看從 NSObject 的初始化了解 isa這篇文章里面的shiftcls分析。
magic
判斷對(duì)象是否初始化完成,在arm64中0x16是調(diào)試器判斷當(dāng)前對(duì)象是真的對(duì)象還是沒有初始化的空間。
weakly_referenced
對(duì)象被指向或者曾經(jīng)指向一個(gè) ARC 的弱變量,沒有弱引用的對(duì)象可以更快釋放
deallocating
對(duì)象是否正在釋放內(nèi)存
has_sidetable_rc
判斷該對(duì)象的引用計(jì)數(shù)是否過大,如果過大則需要其他散列表來進(jìn)行存儲(chǔ)。
extra_rc
存放該對(duì)象的引用計(jì)數(shù)值減一后的結(jié)果。對(duì)象的引用計(jì)數(shù)超過 1,會(huì)存在這個(gè)這個(gè)里面,如果引用計(jì)數(shù)為 10,extra_rc的值就為 9。
ISA_MAGIC_MASK 和 ISA_MASK 分別是通過掩碼的方式獲取MAGIC值 和 isa類指針。
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
return (Class)(isa.bits & ISA_MASK);
}
關(guān)于x86_64的架構(gòu),具體可以看從 NSObject 的初始化了解 isa文章里面的詳細(xì)分析。
(2)cache_t的具體實(shí)現(xiàn)
還是繼續(xù)看源碼
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
typedef unsigned int uint32_t;
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}

根據(jù)源碼,我們可以知道cache_t中存儲(chǔ)了一個(gè)bucket_t的結(jié)構(gòu)體,和兩個(gè)unsigned int的變量。
mask:分配用來緩存bucket的總數(shù)。
occupied:表明目前實(shí)際占用的緩存bucket的個(gè)數(shù)。
bucket_t的結(jié)構(gòu)體中存儲(chǔ)了一個(gè)unsigned long和一個(gè)IMP。IMP是一個(gè)函數(shù)指針,指向了一個(gè)方法的具體實(shí)現(xiàn)。
cache_t中的bucket_t *_buckets其實(shí)就是一個(gè)散列表,用來存儲(chǔ)Method的鏈表。
Cache的作用主要是為了優(yōu)化方法調(diào)用的性能。當(dāng)對(duì)象receiver調(diào)用方法message時(shí),首先根據(jù)對(duì)象receiver的isa指針查找到它對(duì)應(yīng)的類,然后在類的methodLists中搜索方法,如果沒有找到,就使用super_class指針到父類中的methodLists查找,一旦找到就調(diào)用方法。如果沒有找到,有可能消息轉(zhuǎn)發(fā),也可能忽略它。但這樣查找方式效率太低,因?yàn)橥粋€(gè)類大概只有20%的方法經(jīng)常被調(diào)用,占總調(diào)用次數(shù)的80%。所以使用Cache來緩存經(jīng)常調(diào)用的方法,當(dāng)調(diào)用方法時(shí),優(yōu)先在Cache查找,如果沒有找到,再到methodLists查找。
(3)class_data_bits_t的具體實(shí)現(xiàn)
源碼實(shí)現(xiàn)如下:
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
}
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;
}
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_class結(jié)構(gòu)體中的注釋寫到 class_data_bits_t相當(dāng)于 class_rw_t指針加上 rr/alloc 的標(biāo)志。
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
它為我們提供了便捷方法用于返回其中的 class_rw_t *指針:
class_rw_t *data() {
return bits.data();
}
Objc的類的屬性、方法、以及遵循的協(xié)議在obj 2.0的版本之后都放在class_rw_t中。class_ro_t是一個(gè)指向常量的指針,存儲(chǔ)來編譯器決定了的屬性、方法和遵守協(xié)議。rw-readwrite,ro-readonly
在編譯期類的結(jié)構(gòu)中的 class_data_bits_t *data指向的是一個(gè) class_ro_t *指針:

在運(yùn)行時(shí)調(diào)用 realizeClass方法,會(huì)做以下3件事情:
- 從 class_data_bits_t調(diào)用 data方法,將結(jié)果從 class_rw_t強(qiáng)制轉(zhuǎn)換為 class_ro_t指針
- 初始化一個(gè) class_rw_t結(jié)構(gòu)體
- 設(shè)置結(jié)構(gòu)體 ro的值以及 flag
最后調(diào)用methodizeClass方法,把類里面的屬性,協(xié)議,方法都加載進(jìn)來。
struct method_t {
SEL name;
const char *types;
IMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
方法method的定義如上。里面包含3個(gè)成員變量。SEL是方法的名字name。types是Type Encoding類型編碼,類型可參考Type Encoding,在此不細(xì)說。
IMP是一個(gè)函數(shù)指針,指向的是函數(shù)的具體實(shí)現(xiàn)。在runtime中消息傳遞和轉(zhuǎn)發(fā)的目的就是為了找到IMP,并執(zhí)行函數(shù)。
整個(gè)運(yùn)行時(shí)過程可以描述如下:

更加詳細(xì)的分析,請(qǐng)看@Draveness 的這篇文章深入解析 ObjC 中方法的結(jié)構(gòu)
到此,總結(jié)一下objc_class 1.0和2.0的差別。


三. 入院考試

(一)[self class] 與 [super class]
下面代碼輸出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
self和super的區(qū)別:
self是類的一個(gè)隱藏參數(shù),每個(gè)方法的實(shí)現(xiàn)的第一個(gè)參數(shù)即為self。
super并不是隱藏參數(shù),它實(shí)際上只是一個(gè)”編譯器標(biāo)示符”,它負(fù)責(zé)告訴編譯器,當(dāng)調(diào)用方法時(shí),去調(diào)用父類的方法,而不是本類中的方法。
在調(diào)用[super class]的時(shí)候,runtime會(huì)去調(diào)用objc_msgSendSuper方法,而不是objc_msgSend
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
/// 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 */
};
在objc_msgSendSuper方法中,第一個(gè)參數(shù)是一個(gè)objc_super的結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體里面有兩個(gè)變量,一個(gè)是接收消息的receiver,一個(gè)是當(dāng)前類的父類super_class。
入院考試第一題錯(cuò)誤的原因就在這里,誤認(rèn)為[super class]是調(diào)用的[super_class class]。
objc_msgSendSuper的工作原理應(yīng)該是這樣的:
從objc_super結(jié)構(gòu)體指向的superClass父類的方法列表開始查找selector,找到后以objc->receiver去調(diào)用父類的這個(gè)selector。注意,最后的調(diào)用者是objc->receiver,而不是super_class!
那么objc_msgSendSuper最后就轉(zhuǎn)變成
// 注意這里是從父類開始msgSend,而不是從本類開始,謝謝@Josscii 和他同事共同指點(diǎn)出此處描述的不妥。
objc_msgSend(objc_super->receiver, @selector(class))
/// Specifies an instance of a class. 這是類的一個(gè)實(shí)例
__unsafe_unretained id receiver;
// 由于是實(shí)例調(diào)用,所以是減號(hào)方法
- (Class)class {
return object_getClass(self);
}
由于找到了父類NSObject里面的class方法的IMP,又因?yàn)閭魅氲娜雲(yún)bjc_super->receiver = self。self就是son,調(diào)用class,所以父類的方法class執(zhí)行IMP之后,輸出還是son,最后輸出兩個(gè)都一樣,都是輸出son。
(二)isKindOfClass 與 isMemberOfClass
下面代碼輸出什么?
@interface Sark : NSObject
@end
@implementation Sark
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
NSLog(@"%d %d %d %d", res1, res2, res3, res4);
}
return 0;
}
先來分析一下源碼這兩個(gè)函數(shù)的對(duì)象實(shí)現(xiàn)
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
if (isTaggedPointer()) {
uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
return ISA();
}
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
return (Class)(isa.bits & ISA_MASK);
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
首先題目中NSObject 和 Sark分別調(diào)用了class方法。
+ (BOOL)isKindOfClass:(Class)cls方法內(nèi)部,會(huì)先去獲得object_getClass的類,而object_getClass的源碼實(shí)現(xiàn)是去調(diào)用當(dāng)前類的obj->getIsa(),最后在ISA()方法中獲得meta class的指針。
接著在isKindOfClass中有一個(gè)循環(huán),先判斷class是否等于meta class,不等就繼續(xù)循環(huán)判斷是否等于super class,不等再繼續(xù)取super class,如此循環(huán)下去。
[NSObject class]執(zhí)行完之后調(diào)用isKindOfClass,第一次判斷先判斷NSObject 和 NSObject的meta class是否相等,之前講到meta class的時(shí)候放了一張很詳細(xì)的圖,從圖上我們也可以看出,NSObject的meta class與本身不等。接著第二次循環(huán)判斷NSObject與meta class的superclass是否相等。還是從那張圖上面我們可以看到:Root class(meta) 的superclass 就是 Root class(class),也就是NSObject本身。所以第二次循環(huán)相等,于是第一行res1輸出應(yīng)該為YES。
同理,[Sark class]執(zhí)行完之后調(diào)用isKindOfClass,第一次for循環(huán),Sark的Meta Class與[Sark class]不等,第二次for循環(huán),Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark Class不相等。第三次for循環(huán),NSObject Meta Class的super class指向的是NSObject Class,和 Sark Class 不相等。第四次循環(huán),NSObject Class 的super class 指向 nil, 和 Sark Class不相等。第四次循環(huán)之后,退出循環(huán),所以第三行的res3輸出為NO。
如果把這里的Sark改成它的實(shí)例對(duì)象,[sark isKindOfClass:[Sark class],那么此時(shí)就應(yīng)該輸出YES了。因?yàn)樵趇sKindOfClass函數(shù)中,判斷sark的isa指向是否是自己的類Sark,第一次for循環(huán)就能輸出YES了。
isMemberOfClass的源碼實(shí)現(xiàn)是拿到自己的isa指針和自己比較,是否相等。
第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject Class不相等。第四行,isa指向Sark的Meta Class,和Sark Class也不等,所以第二行res2和第四行res4都輸出NO。
(三)Class與內(nèi)存地址
下面的代碼會(huì)?Compile Error / Runtime Crash / NSLog…?
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Sark
- (void)speak {
NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
這道題有兩個(gè)難點(diǎn)。難點(diǎn)一,obj調(diào)用speak方法,到底會(huì)不會(huì)崩潰。難點(diǎn)二,如果speak方法不崩潰,應(yīng)該輸出什么?
首先需要談?wù)勲[藏參數(shù)self和_cmd的問題。
當(dāng)[receiver message]調(diào)用方法時(shí),系統(tǒng)會(huì)在運(yùn)行時(shí)偷偷地動(dòng)態(tài)傳入兩個(gè)隱藏參數(shù)self和_cmd,之所以稱它們?yōu)殡[藏參數(shù),是因?yàn)樵谠创a中沒有聲明和定義這兩個(gè)參數(shù)。self在上面已經(jīng)講解明白了,接下來就來說說_cmd。_cmd表示當(dāng)前調(diào)用方法,其實(shí)它就是一個(gè)方法選擇器SEL。
難點(diǎn)一,能不能調(diào)用speak方法?
id cls = [Sark class];
void *obj = &cls;
答案是可以的。obj被轉(zhuǎn)換成了一個(gè)指向Sark Class的指針,然后使用id轉(zhuǎn)換成了objc_object類型。obj現(xiàn)在已經(jīng)是一個(gè)Sark類型的實(shí)例對(duì)象了。當(dāng)然接下來可以調(diào)用speak的方法。
難點(diǎn)二,如果能調(diào)用speak,會(huì)輸出什么呢?
很多人可能會(huì)認(rèn)為會(huì)輸出sark相關(guān)的信息。這樣答案就錯(cuò)誤了。
正確的答案會(huì)輸出
my name is <ViewController: 0x7ff6d9f31c50>
內(nèi)存地址每次運(yùn)行都不同,但是前面一定是ViewController。why?
我們把代碼改變一下,打印更多的信息出來。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"ViewController = %@ , 地址 = %p", self, &self);
id cls = [Sark class];
NSLog(@"Sark class = %@ 地址 = %p", cls, &cls);
void *obj = &cls;
NSLog(@"Void *obj = %@ 地址 = %p", obj,&obj);
[(__bridge id)obj speak];
Sark *sark = [[Sark alloc]init];
NSLog(@"Sark instance = %@ 地址 = %p",sark,&sark);
[sark speak];
}
我們把對(duì)象的指針地址都打印出來。輸出結(jié)果:
ViewController = <ViewController: 0x7fb570e2ad00> , 地址 = 0x7fff543f5aa8
Sark class = Sark 地址 = 0x7fff543f5a88
Void *obj = <Sark: 0x7fff543f5a88> 地址 = 0x7fff543f5a80
my name is <ViewController: 0x7fb570e2ad00>
Sark instance = <Sark: 0x7fb570d20b10> 地址 = 0x7fff543f5a78
my name is (null)

// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
objc_msgSendSuper2方法入?yún)⑹且粋€(gè)objc_super *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
所以按viewDidLoad執(zhí)行時(shí)各個(gè)變量入棧順序從高到底為self, _cmd, super_class(等同于self.class), receiver(等同于self), obj。

第一個(gè)self和第二個(gè)_cmd是隱藏參數(shù)。第三個(gè)self.class和第四個(gè)self是[super viewDidLoad]方法執(zhí)行時(shí)候的參數(shù)。
在調(diào)用self.name的時(shí)候,本質(zhì)上是self指針在內(nèi)存向高位地址偏移一個(gè)指針。

從打印結(jié)果我們可以看到,obj就是cls的地址。在obj向上偏移一個(gè)指針就到了0x7fff543f5a90,這正好是ViewController的地址。
所以輸出為my name is <ViewController: 0x7fb570e2ad00>。
至此,Objc中的對(duì)象到底是什么呢?
實(shí)質(zhì):Objc中的對(duì)象是一個(gè)指向ClassObject地址的變量,即 id obj = &ClassObject , 而對(duì)象的實(shí)例變量 void *ivar = &obj + offset(N)
加深一下對(duì)上面這句話的理解,下面這段代碼會(huì)輸出什么?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"ViewController = %@ , 地址 = %p", self, &self);
NSString *myName = @"halfrost";
id cls = [Sark class];
NSLog(@"Sark class = %@ 地址 = %p", cls, &cls);
void *obj = &cls;
NSLog(@"Void *obj = %@ 地址 = %p", obj,&obj);
[(__bridge id)obj speak];
Sark *sark = [[Sark alloc]init];
NSLog(@"Sark instance = %@ 地址 = %p",sark,&sark);
[sark speak];
}
ViewController = <ViewController: 0x7fff44404ab0> , 地址 = 0x7fff56a48a78
Sark class = Sark 地址 = 0x7fff56a48a50
Void *obj = <Sark: 0x7fff56a48a50> 地址 = 0x7fff56a48a48
my name is halfrost
Sark instance = <Sark: 0x6080000233e0> 地址 = 0x7fff56a48a40
my name is (null)
由于加了一個(gè)字符串,結(jié)果輸出就完全變了,[(__bridge id)obj speak];這句話會(huì)輸出“my name is halfrost”
原因還是和上面的類似。按viewDidLoad執(zhí)行時(shí)各個(gè)變量入棧順序從高到底為self,_cmd,self.class( super_class ),self ( receiver ),myName,obj。obj往上偏移一個(gè)指針,就是myName字符串,所以輸出變成了輸出myName了。

這里有一點(diǎn)需要額外說明的是,棧里面有兩個(gè) self,可能有些人認(rèn)為是指針偏移到了第一個(gè) self 了,于是打印出了 ViewController:
my name is <ViewController: 0x7fb570e2ad00>

其實(shí)這種想法是不對(duì)的,從 obj 往上找 name 屬性,完全是指針偏移了一個(gè) offset 導(dǎo)致的,也就是說指針只往下偏移了一個(gè)。那么怎么證明指針只偏移了一個(gè),而不是偏移了4個(gè)到最下面的 self 呢?

obj 的地址是 0x7fff5c7b9a08,self 的地址是 0x7fff5c7b9a28。每個(gè)指針占8個(gè)字節(jié),所以從 obj 到 self 中間確實(shí)有4個(gè)指針大小的間隔。如果從 obj 偏移一個(gè)指針,就到了 0x7fff5c7b9a10。我們需要把這個(gè)內(nèi)存地址里面的內(nèi)容打印出來。
LLDB 調(diào)試中,可以使用examine命令(簡寫是x)來查看內(nèi)存地址中的值。x命令的語法如下所示:
x/
n、f、u是可選的參數(shù)。
n 是一個(gè)正整數(shù),表示顯示內(nèi)存的長度,也就是說從當(dāng)前地址向后顯示幾個(gè)地址的內(nèi)容。
f 表示顯示的格式,參見上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是 i。
u 表示從當(dāng)前地址往后請(qǐng)求的字節(jié)數(shù),如果不指定的話,GDB默認(rèn)是4個(gè)bytes。u參數(shù)可以用下面的字符來代替,b表示單字節(jié),h表示雙字節(jié),w表示四字節(jié),g表示八字節(jié)。當(dāng)我們指定了字節(jié)長度后,GDB會(huì)從指內(nèi)存定的內(nèi)存地址開始,讀寫指定字節(jié),并把其當(dāng)作一個(gè)值取出來。

我們用 x 命令分別打印出 0x7fff5c7b9a10 和 0x7fff5c7b9a28 內(nèi)存地址里面的內(nèi)容,我們會(huì)發(fā)現(xiàn)兩個(gè)打印出來的值是一樣的,都是 0x7fbf0d606aa0。
這兩個(gè) self 的地址不同,里面存儲(chǔ)的內(nèi)容是相同的。
所以 obj 是偏移了一個(gè)指針,而不是偏移到最下面的 self 。
最后
入院考試由于還有一題沒有解答出來,所以醫(yī)院決定讓我住院一天觀察。
未完待續(xù),請(qǐng)大家多多指教。