我第一次開始重視Objective-C Runtime是從2014年11月1日,@唐巧老師在微博上發的一條微博開始。html
這是sunnyxx在線下的一次分享會。會上還給了4道題目。git
這4道題以我當時的知識,不少就不肯定,拿不許。從此次入院考試開始,就成功入院了。後來這兩年對Runtime的理解慢慢增長了,打算今天本身總結總結平時一直躺在我印象筆記裏面的筆記。有些人可能有疑惑,學習Runtime到底有啥用,平時好像並不會用到。但願看完我此次的總結,心中能解開一些疑惑。github
Runtime 又叫運行時,是一套底層的 C 語言 API,是 iOS 系統的核心之一。開發者在編碼過程當中,能夠給任意一個對象發送消息,在編譯階段只是肯定了要向接收者發送這條消息,而接受者將要如何響應和處理這條消息,那就要看運行時來決定了。objective-c
C語言中,在編譯期,函數的調用就會決定調用哪一個函數。 而OC的函數,屬於動態調用過程,在編譯期並不能決定真正調用哪一個函數,只有在真正運行時纔會根據函數的名稱找到對應的函數來調用。swift
Objective-C 是一個動態語言,這意味着它不只須要一個編譯器,也須要一個運行時系統來動態得建立類和對象、進行消息傳遞和轉發。vim
Objc 在三種層面上與 Runtime 系統進行交互:緩存
通常狀況開發者只須要編寫 OC 代碼便可,Runtime 系統自動在幕後把咱們寫的源代碼在編譯階段轉換成運行時代碼,在運行時肯定對應的數據結構和調用具體哪一個方法。數據結構
在OC的世界中,除了NSProxy類之外,全部的類都是NSObject的子類。在Foundation框架下,NSObject和NSProxy兩個基類,定義了類層次結構中該類下方全部類的公共接口和行爲。NSProxy是專門用於實現代理對象的類,這個類暫時本篇文章不提。這兩個類都遵循了NSObject協議。在NSObject協議中,聲明瞭全部OC對象的公共方法。架構
在NSObject協議中,有如下5個方法,是能夠從Runtime中獲取信息,讓對象進行自我檢查。app
- (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。
以上這些方法會在本篇文章中詳細分析具體實現。
關於庫函數能夠在Objective-C Runtime Reference中查看 Runtime 函數的詳細文檔。
關於這一點,其實還有一個小插曲。當咱們導入了objc/Runtime.h和objc/message.h兩個頭文件以後,咱們查找到了Runtime的函數以後,代碼打完,發現沒有代碼提示了,那些函數裏面的參數和描述都沒有了。對於熟悉Runtime的開發者來講,這並無什麼難的,由於參數早已銘記於胸。可是對於新手來講,這是至關不友好的。並且,若是是從iOS6開始開發的同窗,依稀可能能感覺到,關於Runtime的具體實現的官方文檔愈來愈少了?可能還懷疑是否是錯覺。其實從Xcode5開始,蘋果就不建議咱們手動調用Runtime的API,也一樣但願咱們不要知道具體底層實現。因此IDE上面默認代了一個參數,禁止了Runtime的代碼提示,源碼和文檔方面也刪除了一些解釋。
具體設置以下:
若是發現導入了兩個庫文件以後,仍然沒有代碼提示,就須要把這裏的設置改爲NO,便可。
由上面一章節,咱們知道了與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指針。
咱們其實應該明白,類對象和元類對象是惟一的,對象是能夠在運行時建立無數個的。而在main方法執行以前,從 dyld到runtime這期間,類對象和元類對象在這期間被建立。具體可看sunnyxx這篇iOS 程序 main 函數以前發生了什麼
接下來咱們就該研究研究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文章裏面的詳細分析。
仍是繼續看源碼
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查找。
源碼實現以下:
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件事情:
最後調用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的差異。
下面代碼輸出什麼?
@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。
下面代碼輸出什麼?
@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。
下面的代碼會?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
入院考試因爲還有一題沒有解答出來,因此醫院決定讓我住院一天觀察。
未完待續,請你們多多指教。