前言
我第一次開始重視Objective-C Runtime是從2014年11月1日,@唐巧老師在微博上發的一條微博開始。
這是sunnyxx線上下的一次分享會。會上還給了4道題目。
這4道題以我當時的知識,很多就不確定,拿不準。從這次入院考試開始,就成功入院了。後來這兩年對Runtime的理解慢慢增加了,打算今天自己總結總結平時一直躺在我印象筆記裡面的筆記。有些人可能有疑惑,學習Runtime到底有啥用,平時好像並不會用到。希望看完我這次的總結,心中能解開一些疑惑。
目錄
- 1.Runtime簡介
- 2.NSObject起源
- (1) isa_t結構體的具體實現
- (2) cache_t的具體實現
- (3) class_data_bits_t的具體實現
- 3.入院考試
一. Runtime簡介
Runtime 又叫執行時,是一套底層的 C 語言 API,是 iOS 系統的核心之一。開發者在編碼過程中,可以給任意一個物件傳送訊息,在編譯階段只是確定了要向接收者傳送這條訊息,而接受者將要如何響應和處理這條訊息,那就要看執行時來決定了。
C語言中,在編譯期,函式的呼叫就會決定呼叫哪個函式。
而OC的函式,屬於動態呼叫過程,在編譯期並不能決定真正呼叫哪個函式,只有在真正執行時才會根據函式的名稱找到對應的函式來呼叫。
Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個執行時系統來動態得建立類和物件、進行訊息傳遞和轉發。
Objc 在三種層面上與 Runtime 系統進行互動:
1. 通過 Objective-C 原始碼
一般情況開發者只需要編寫 OC 程式碼即可,Runtime 系統自動在幕後把我們寫的原始碼在編譯階段轉換成執行時程式碼,在執行時確定對應的資料結構和呼叫具體哪個方法。
2. 通過 Foundation 框架的 NSObject 類定義的方法
在OC的世界中,除了NSProxy類以外,所有的類都是NSObject的子類。在Foundation框架下,NSObject和NSProxy兩個基類,定義了類層次結構中該類下方所有類的公共介面和行為。NSProxy是專門用於實現代理物件的類,這個類暫時本篇文章不提。這兩個類都遵循了NSObject協議。在NSObject協議中,宣告瞭所有OC物件的公共方法。
在NSObject協議中,有以下5個方法,是可以從Runtime中獲取資訊,讓物件進行自我檢查。
- (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方法返回物件的類;
-isKindOfClass: 和 -isMemberOfClass: 方法檢查物件是否存在於指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變數);
-respondsToSelector: 檢查物件能否響應指定的訊息;
-conformsToProtocol:檢查物件是否實現了指定協議類的方法;
在NSObject的類中還定義了一個方法
- (IMP)methodForSelector:(SEL)aSelector;複製程式碼
這個方法會返回指定方法實現的地址IMP。
以上這些方法會在本篇文章中詳細分析具體實現。
3. 通過對 Runtime 庫函式的直接呼叫
關於庫函式可以在Objective-C Runtime Reference中檢視 Runtime 函式的詳細文件。
關於這一點,其實還有一個小插曲。當我們匯入了objc/Runtime.h和objc/message.h兩個標頭檔案之後,我們查詢到了Runtime的函式之後,程式碼打完,發現沒有程式碼提示了,那些函式裡面的引數和描述都沒有了。對於熟悉Runtime的開發者來說,這並沒有什麼難的,因為引數早已銘記於胸。但是對於新手來說,這是相當不友好的。而且,如果是從iOS6開始開發的同學,依稀可能能感受到,關於Runtime的具體實現的官方文件越來越少了?可能還懷疑是不是錯覺。其實從Xcode5開始,蘋果就不建議我們手動呼叫Runtime的API,也同樣希望我們不要知道具體底層實現。所以IDE上面預設代了一個引數,禁止了Runtime的程式碼提示,原始碼和文件方面也刪除了一些解釋。
具體設定如下:
如果發現匯入了兩個庫檔案之後,仍然沒有程式碼提示,就需要把這裡的設定改成NO,即可。
二. NSObject起源
由上面一章節,我們知道了與Runtime互動有3種方式,前兩種方式都與NSObject有關,那我們就從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;複製程式碼
在這裡可以看到,在一個類中,有超類的指標,類名,版本的資訊。
ivars是objc_ivar_list成員變數列表的指標;methodLists是指向objc_method_list指標的指標。*methodLists是指向方法列表的指標。這裡如果動態修改*methodLists的值來新增成員方法,這也是Category實現的原理,同樣解釋了Category不能新增屬性的原因。
關於Category,這裡推薦2篇文章可以仔細研讀一下。
深入理解Objective-C:Category
結合 Category 工作原理分析 OC2.0 中的 runtime
然後在2006年蘋果釋出Objc 2.0之後,objc_class的定義就變成下面這個樣子了。
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;
}複製程式碼
把原始碼的定義轉化成類圖,就是上圖的樣子。
從上述原始碼中,我們可以看到,Objective-C 物件都是 C 語言結構體實現的,在objc2.0中,所有的物件都會包含一個isa_t型別的結構體。
objc_object被原始碼typedef成了id型別,這也就是我們平時遇到的id型別。這個結構體中就只包含了一個isa_t型別的結構體。這個結構體在下面會詳細分析。
objc_class繼承於objc_object。所以在objc_class中也會包含isa_t型別的結構體isa。至此,可以得出結論:Objective-C 中類也是一個物件。在objc_class中,除了isa之外,還有3個成員變數,一個是父類的指標,一個是方法快取,最後一個這個類的例項方法連結串列。
object類和NSObject類裡面分別都包含一個objc_class型別的isa。
上圖的左半邊類的關係描述完了,接著先從isa來說起。
當一個物件的例項方法被呼叫的時候,會通過isa找到相應的類,然後在該類的class_data_bits_t中去查詢方法。class_data_bits_t是指向了類物件的資料區域。在該資料區域內查詢相應方法的對應實現。
但是在我們呼叫類方法的時候,類物件的isa裡面是什麼呢?這裡為了和物件查詢方法的機制一致,遂引入了元類(meta-class)的概念。
關於元類,更多具體可以研究這篇文章What is a meta-class in Objective-C?
在引入元類之後,類物件和物件查詢方法的機制就完全統一了。
物件的例項方法呼叫時,通過物件的 isa 在類中獲取方法的實現。
類物件的類方法呼叫時,通過類的 isa 在元類中獲取方法的實現。
meta-class之所以重要,是因為它儲存著一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。
對應關係的圖如下圖,下圖很好的描述了物件,類,元類之間的關係:
圖中實線是 super_class指標,虛線是isa指標。
- Root class (class)其實就是NSObject,NSObject是沒有超類的,所以Root class(class)的superclass指向nil。
- 每個Class都有一個isa指標指向唯一的Meta class
- Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個迴路。
- 每個Meta class的isa指標都指向Root class (meta)。
我們其實應該明白,類物件和元類物件是唯一的,物件是可以在執行時建立無數個的。而在main方法執行之前,從 dyld到runtime這期間,類物件和元類物件在這期間被建立。具體可看sunnyxx這篇iOS 程式 main 函式之前發生了什麼
(1)isa_t結構體的具體實現
接下來我們就該研究研究isa的具體實現了。objc_object裡面的isa是isa_t型別。通過檢視原始碼,我們可以知道isa_t是一個union聯合體。
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第二個引數傳入了一個true,所以initIsa就會執行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轉換成二進位制是11010000000000000000000000000000000000001,結構如下圖:
關於引數的說明:
第一位index,代表是否開啟isa指標優化。index = 1,代表開啟isa指標優化。
在2013年9月,蘋果推出了iPhone5s,與此同時,iPhone5s配備了首個採用64位架構的A7雙核處理器,為了節省記憶體和提高執行效率,蘋果提出了Tagged Pointer的概念。對於64位程式,引入Tagged Pointer後,相關邏輯能減少一半的記憶體佔用,以及3倍的訪問速度提升,100倍的建立、銷燬速度提升。
在WWDC2013的《Session 404 Advanced in Objective-C》視訊中,蘋果介紹了 Tagged Pointer。 Tagged Pointer的存在主要是為了節省記憶體。我們知道,物件的指標大小一般是與機器字長有關,在32位系統中,一個指標的大小是32位(4位元組),而在64位系統中,一個指標的大小將是64位(8位元組)。
假設我們要儲存一個NSNumber物件,其值是一個整數。正常情況下,如果這個整數只是一個NSInteger的普通變數,那麼它所佔用的記憶體是與CPU的位數有關,在32位CPU下佔4個位元組,在64位CPU下是佔8個位元組的。而指標型別的大小通常也是與CPU位數相關,一個指標所佔用的記憶體在32位CPU下為4個位元組,在64位CPU下也是8個位元組。如果沒有Tagged Pointer物件,從32位機器遷移到64位機器中後,雖然邏輯沒有任何變化,但這種NSNumber、NSDate一類的物件所佔用的記憶體會翻倍。如下圖所示:
蘋果提出了Tagged Pointer物件。由於NSNumber、NSDate一類的變數本身的值需要佔用的記憶體大小常常不需要8個位元組,拿整數來說,4個位元組所能表示的有符號整數就可以達到20多億(注:2^31=2147483648,另外1位作為符號位),對於絕大多數情況都是可以處理的。所以,引入了Tagged Pointer物件之後,64位CPU下NSNumber的記憶體圖變成了以下這樣:
關於Tagged Pointer技術詳細的,可以看上面連結那個文章。
has_assoc
物件含有或者曾經含有關聯引用,沒有關聯引用的可以更快地釋放記憶體
has_cxx_dtor
表示該物件是否有 C++ 或者 Objc 的析構器
shiftcls
類的指標。arm64架構中有33位可以儲存類指標。
原始碼中isa.shiftcls = (uintptr_t)cls >> 3;
將當前地址右移三位的主要原因是用於將 Class 指標中無用的後三位清除減小記憶體的消耗,因為類的指標要按照位元組(8 bits)對齊記憶體,其指標後三位都是沒有意義的 0。具體可以看從 NSObject 的初始化了解 isa這篇文章裡面的shiftcls分析。
magic
判斷物件是否初始化完成,在arm64中0x16是偵錯程式判斷當前物件是真的物件還是沒有初始化的空間。
weakly_referenced
物件被指向或者曾經指向一個 ARC 的弱變數,沒有弱引用的物件可以更快釋放
deallocating
物件是否正在釋放記憶體
has_sidetable_rc
判斷該物件的引用計數是否過大,如果過大則需要其他雜湊表來進行儲存。
extra_rc
存放該物件的引用計數值減一後的結果。物件的引用計數超過 1,會存在這個這個裡面,如果引用計數為 10,extra_rc的值就為 9。
ISA_MAGIC_MASK 和 ISA_MASK 分別是通過掩碼的方式獲取MAGIC值 和 isa類指標。
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
return (Class)(isa.bits & ISA_MASK);
}複製程式碼
關於x86_64的架構,具體可以看從 NSObject 的初始化了解 isa文章裡面的詳細分析。
(2)cache_t的具體實現
還是繼續看原始碼
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;
}複製程式碼
根據原始碼,我們可以知道cache_t中儲存了一個bucket_t的結構體,和兩個unsigned int的變數。
mask:分配用來快取bucket的總數。
occupied:表明目前實際佔用的快取bucket的個數。
bucket_t的結構體中儲存了一個unsigned long和一個IMP。IMP是一個函式指標,指向了一個方法的具體實現。
cache_t中的bucket_t *_buckets其實就是一個雜湊表,用來儲存Method的連結串列。
Cache的作用主要是為了優化方法呼叫的效能。當物件receiver呼叫方法message時,首先根據物件receiver的isa指標查詢到它對應的類,然後在類的methodLists中搜尋方法,如果沒有找到,就使用super_class指標到父類中的methodLists查詢,一旦找到就呼叫方法。如果沒有找到,有可能訊息轉發,也可能忽略它。但這樣查詢方式效率太低,因為往往一個類大概只有20%的方法經常被呼叫,佔總呼叫次數的80%。所以使用Cache來快取經常呼叫的方法,當呼叫方法時,優先在Cache查詢,如果沒有找到,再到methodLists查詢。
(3)class_data_bits_t的具體實現
原始碼實現如下:
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結構體中的註釋寫到 class_data_bits_t相當於 class_rw_t指標加上 rr/alloc 的標誌。
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags複製程式碼
它為我們提供了便捷方法用於返回其中的 class_rw_t *指標:
class_rw_t *data() {
return bits.data();
}複製程式碼
Objc的類的屬性、方法、以及遵循的協議在obj 2.0的版本之後都放在class_rw_t中。class_ro_t是一個指向常量的指標,儲存來編譯器決定了的屬性、方法和遵守協議。rw-readwrite,ro-readonly
在編譯期類的結構中的 class_data_bits_t *data指向的是一個 class_ro_t *指標:
在執行時呼叫 realizeClass方法,會做以下3件事情:
- 從 class_data_bits_t呼叫 data方法,將結果從 class_rw_t強制轉換為 class_ro_t指標
- 初始化一個 class_rw_t結構體
- 設定結構體 ro的值以及 flag
最後呼叫methodizeClass方法,把類裡面的屬性,協議,方法都載入進來。
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個成員變數。SEL是方法的名字name。types是Type Encoding型別編碼,型別可參考Type Encoding,在此不細說。
IMP是一個函式指標,指向的是函式的具體實現。在runtime中訊息傳遞和轉發的目的就是為了找到IMP,並執行函式。
整個執行時過程可以描述如下:
更加詳細的分析,請看@Draveness 的這篇文章深入解析 ObjC 中方法的結構
到此,總結一下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的區別:
self是類的一個隱藏引數,每個方法的實現的第一個引數即為self。
super並不是隱藏引數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當呼叫方法時,去呼叫父類的方法,而不是本類中的方法。
在呼叫[super class]的時候,runtime會去呼叫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方法中,第一個引數是一個objc_super的結構體,這個結構體裡面有兩個變數,一個是接收訊息的receiver,一個是
當前類的父類super_class。
入院考試第一題錯誤的原因就在這裡,誤認為[super class]是呼叫的[super_class class]。
objc_msgSendSuper的工作原理應該是這樣的:
從objc_super結構體指向的superClass父類的方法列表開始查詢selector,找到後以objc->receiver去呼叫這個selector。注意,最後的呼叫者是objc->receiver,而不是super_class!
那麼objc_msgSendSuper最後就轉變成
objc_msgSend(objc_super->receiver, @selector(class))複製程式碼
objc_super->receiver = self。所以最後輸出兩個都一樣,都是輸出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; }複製程式碼
先來分析一下原始碼這兩個函式的物件實現
+ (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分別呼叫了class方法。
+ (BOOL)isKindOfClass:(Class)cls方法內部,會先去獲得object_getClass的類,而object_getClass的原始碼實現是去呼叫當前類的obj->getIsa(),最後在ISA()方法中獲得meta class的指標。
接著在isKindOfClass中有一個迴圈,先判斷class是否等於meta class,不等就繼續迴圈判斷是否等於super class,不等再繼續取super class,如此迴圈下去。
[NSObject class]執行完之後呼叫isKindOfClass,第一次判斷先判斷NSObject 和 NSObject的meta class是否相當,之前講到meta class的時候放了一張很詳細的圖,從圖上我們也可以看出,NSObject的meta class與本身不等。接著第二次迴圈判斷NSObject與meta class的superclass是否相當。還是從那張圖上面我們可以看到:Root class(meta) 的superclass 就是 Root class(class),也就是NSObject本身。所以第二次迴圈相等,於是第一行res1輸出應該為YES。
同理,[Sark class]執行完之後呼叫isKindOfClass,第一次for迴圈,Sark的Meta Class與[Sark class]不等,第二次for迴圈,Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark Class不相等。第三次for迴圈,NSObject Meta Class的super class指向的是NSObject Class,和 Sark Class 不相等。第四次迴圈,NSObject Class 的super class 指向 nil, 和 Sark Class不相等。第四次迴圈之後,退出迴圈,所以第三行的res3輸出為NO。
如果把這裡的Sark改成它的例項物件,[sark isKindOfClass:[Sark class],那麼此時就應該輸出YES了。因為在isKindOfClass函式中,判斷sark的meta class是自己的元類Sark,第一次for迴圈就能輸出YES了。
isMemberOfClass的原始碼實現是拿到自己的isa指標和自己比較,是否相等。
第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject Class不相等。第四行,isa指向Sark的Meta Class,和Sark Class也不等,所以第二行res2和第四行res4都輸出NO。
(三)Class與記憶體地址
下面的程式碼會?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複製程式碼
這道題有兩個難點。難點一,obj呼叫speak方法,到底會不會崩潰。難點二,如果speak方法不崩潰,應該輸出什麼?
首先需要談談隱藏引數self和_cmd的問題。
當[receiver message]呼叫方法時,系統會在執行時偷偷地動態傳入兩個隱藏引數self和_cmd,之所以稱它們為隱藏引數,是因為在原始碼中沒有宣告和定義這兩個引數。self在上面已經講解明白了,接下來就來說說_cmd。_cmd表示當前呼叫方法,其實它就是一個方法選擇器SEL。
難點一,能不能呼叫speak方法?
id cls = [Sark class];
void *obj = &cls;複製程式碼
答案是可以的。obj被轉換成了一個指向Sark Class的指標,然後使用id轉換成了objc_object型別。obj現在已經是一個Sark型別的例項物件了。當然接下來可以呼叫speak的方法。
難點二,如果能呼叫speak,會輸出什麼呢?
很多人可能會認為會輸出sark相關的資訊。這樣答案就錯誤了。
正確的答案會輸出
my name is <ViewController: 0x7ff6d9f31c50>複製程式碼
記憶體地址每次執行都不同,但是前面一定是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];
}複製程式碼
我們把物件的指標地址都列印出來。輸出結果:
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)複製程式碼
按viewDidLoad執行時各個變數入棧順序從高到底為self, _cmd, self.class, self, obj。
第一個self和第二個_cmd是隱藏引數。第三個self.class和第四個self是[super viewDidLoad]方法執行時候的引數。
在呼叫self.name的時候,本質上是self指標在記憶體向高位地址偏移一個指標。在32位下面,一個指標是4位元組=4*8bit=32bit。
從列印結果我們可以看到,obj就是cls的地址。在obj向上偏移32bit就到了0x7fff543f5aa8,這正好是ViewController的地址。
所以輸出為my name is
入院考試由於還有一題沒有解答出來,所以醫院決定讓我住院一天觀察。
未完待續,請大家多多指教。