經過對類,Runtime等底層的相關探索,對原理已經有了掌握,本章經過幾個面試題來加深印象,會保持持續更新。面試
先上幾篇原理文章,對面試題的理解會更深入緩存
傳送門☞iOS底層學習 - Runtime之方法消息的前世此生(一)bash
傳送門☞iOS底層學習 - Runtime之方法消息的前世此生(二)markdown
答:是由C 和C++ 彙編 實現的⼀套API,爲OC語⾔加⼊了⾯向對象,運⾏時的功能。平時編寫的OC代碼,在程序運⾏過程當中,其實最終會轉換成Runtime的C語⾔代 碼,Runtime
是 Objective-C
的幕後⼯做者。app
好比:將數據類型的肯定由編譯時推遲到了運⾏時,比較典型的就是類的ro
和rw
屬性。ro
在編譯期就肯定好(read-only),而rw
是運行時才肯定,能夠進行修改(read-write)。像下面的例子就比較典型👇框架
方法的本質就是消息的發送,就底層_objc_msgSennd
方法尋找方法IMP的過程,主要經歷瞭如下幾個步驟:ide
objc_msgSend
)查找cache_t
中緩存的消息lookUpImpOrForward
遞歸查找當前類和父類的rw
中methodlist
的方法resolveInstanceMethod
方法,來實現消息動態處理CoreFoundation
框架來觸發消息轉發流程,forwardingTargetForSelector
實現快速轉發,其餘類可實現處理方法methodSignatureForSelector
方法,來獲取到方法的簽名,從在生成相對應的invocation
,經過forwardInvocation
來對invocation
進行處理,通常處置崩潰都在此處理SEL
是方法編號,也是方法名,在dyld加載鏡像帶內存時,經過_read_image
方法加載到內存的表中了函數
IMP
就是咱們函數實現指針 ,找IMP
就是找函數的過程oop
SEL
就至關於書本的⽬錄 tittle,post
IMP
就是書本的⻚碼,
函數
就是具體頁碼對應的實現內容
查找具體的函數就是想看這本書⾥⾯具體篇章的內容
不能。
由於咱們編譯好的實例變量存儲的位置在ro
,⼀旦編譯完成,內存結構就徹底肯定 就⽆法修改,只能修改rw
中的方法或者能夠經過關聯對象的方式來添加屬性
關聯對象添加的主要步驟以下:
objc_setAssociatedObject
設置set
方法:找到關聯對象的總哈希表,而後經過指針地址找到該類的哈希表,而後經過key值進行存儲objc_getAssociatedObject
設置get
方法:和set方法同樣查詢表,找到值dealloc
會清除關聯對象的哈希表isKindOfClass
和 isMemberOfClass
區別先上例子🌰
1.第一個
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
複製代碼
👆上述Log的打印結果爲1,0,0,0
2.第二個
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; // 1
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; // 1
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]]; // 1
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]]; // 1
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
複製代碼
👆上述Log的打印結果爲1,1,1,1
相信對於第二個例子,你們都使用的很是熟練。咱們發現兩個例子的主要區別在於消息接受者是實例對象仍是類對象,打印結果,咱們看源碼探究
既然[NSObject class]
類也能夠調用這兩個方法,說明這兩個方法是有對應的類方法和實例方法的,只不過咱們平時不適用類方法而已😂
/*********************************************************************** * object_getClass. * Locking: None. If you add locking, tell gdb (rdar://7516456). **********************************************************************/ Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; } 複製代碼
✅//object_getClass()取得的是對象的isa指針指向的對象,也就是判斷傳入的類對象的元類對象是否與傳入的這個對象相等,因此這個cls應該是元類對象纔有可能相等 + (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } ✅//判斷傳入的實例對象的類對象是否與傳入的對象相等,因此cls只有多是類對象纔有可能相等 - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; } ✅//循環判斷傳入的類對象的元類對象及其父類的元類對象是否等於傳入的cls + (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } ✅//循環判斷實例對象的父類的類對象是否等於傳入的對象cls,也就是判斷實例對象是不是cls及其子類的一種 - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } 複製代碼
經過這個兩個方法的源碼咱們能夠知道,
isMemberOfClass:
是檢測方法調用者對象的類是否等於傳入的這個類。isKindOfClass:
是判斷方法調用者對象的類是否等於傳入的這個類或者其子類。還有一個適用於這四個方法的一點是,若是方法調用者是實例對象,那麼傳入的就應該是類對象;若是方法調用者是類對象,那麼傳入的就應該是元類對象。
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
爲啥打印是1呢?答:[NSObject class]
類對象調用isKindOfClass
代表其元類會遞歸判斷是否等於當前類或者其父類,咱們知道NSObject
的元類爲根元類,根據繼承鏈關係根元類的父類即爲NSObject
類對象,即[NSObject class]
,因此相等
建立一個Student
類繼承子Person
類,下面代碼打印出什麼
NSLog(@"[self class] = %@", [self class]); NSLog(@"[super class] = %@", [super class]); NSLog(@"[self superclass] = %@", [self superclass]); NSLog(@"[super superclass] = %@", [super superclass]); 複製代碼
先上正確答案:
2020-01-17 15:54:02.224686+0800 TEST[8409:174143] [self class] = Student
2020-01-17 15:54:02.224922+0800 TEST[8409:174143] [super class] = Student
2020-01-17 15:54:02.225040+0800 TEST[8409:174143] [self superclass] = Person
2020-01-17 15:54:02.225922+0800 TEST[8409:174143] [super superclass] = Person
複製代碼
咱們發現第二個和第四個和咱們猜測的貌似不太同樣,並非Person
,先上源碼看一下第一個和第三個的理解
/******************************************************* ✅ //經過對象的isa指針獲取類的類對象 Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; } + (Class)class { return self; } - (Class)class { return object_getClass(self); } + (Class)superclass { return self->superclass; } - (Class)superclass { return [self class]->superclass; } Class class_getSuperclass(Class cls) { if (!cls) return nil; return cls->superclass; } ******************************************************/ 複製代碼
咱們知道,這裏方法中的self
都是指消息的接受者,在問題中就表示Student
類,根據以上源碼,第一個和第三個沒啥疑問,都是尋找Student
的isa
和父類,那麼super
調用時,有啥不一樣,咱們須要先知道super
的本質
經過clang編譯代碼,咱們能夠發現,底層調用時super
調用的方法不是magSend
,而是objc_msgSendSuper
,傳入了一個super
的結構體和方法,而super
的結構體以下
objc_msgSendSuper(object ,superclass, @selector(class))
複製代碼
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
複製代碼
兩個參數分別爲消息的接受者和父類,在這裏Student
即爲消息接受者,Person
爲父類.
咱們知道消息發送的時候,慢速查找流程是須要從自身遞歸查找到NSObject
的,而objc_msgSendSuper
就表示直接從消息接受者的父類開始遞歸查找,跳過了自己的方法列表,這樣查找的速度能夠更快
因此調用[super class]
和[super superclass]
本質上消息的接受者仍是self
,即Student
類,因爲class
方法的實現,實際上是在基類NSObject
中的,因此不論是從Student
類方法列表開始查詢,仍是從父類Person
方法列表查詢,最終都會走到基類中的class
方法,而此方法根據源碼就是尋找消息接受者的isa
和superclass
,因此出現上述的打印結果
[self class]
就是發送消息objc_msgSend
,消息接受者是 self
,⽅法編號:class
[super class]
本質就是objc_msgSendSuper
, 消息的接受者仍是 self
⽅法編號:class
只是objc_msgSendSuper
會更快 直接跳過 self
的查找,可是都會走到NSObject
基類的實現方法中,可是都是以self
爲接受者
主要總結以下:
經過SideTable
找到咱們的weak_table
weak_table
根據referent
找到或者建立 weak_entry_t
而後append_referrer(entry, referrer)
將新弱引⽤的對象加進去entry
最後weak_entry_insert
把entry
加⼊到咱們的weak_table
在類dealloc
時,會根據插入的步驟找到對應的弱引用,並置爲nil
關於weak
的相關知識作了單獨的總結,詳情能夠看下方文章👇
關於Method Swizzling
方法交換的總結,詳情能夠看下面的文章:
iOS底層學習 - Runtime之Method Swizzling黑魔法
***********************LGPerson*************************** @interface LGPerson : NSObject @property (nonatomic, copy)NSString *name; //@property (nonatomic, copy)NSString *subject; //@property (nonatomic)int age; - (void)print; @end @implementation LGPerson - (void)print{ NSLog(@"NB %s - %@",__func__,self.name); } @end **************************調用***************************** - (void)viewDidLoad { [super viewDidLoad]; NSString *tem = @"WY"; id cls = [LGPerson class]; void *obj= &cls; [(__bridge id)obj print]; // LGPerson *person = [LGPerson alloc]; // [person print]; } 複製代碼
2020-01-20 17:40:15.322404+0800 LGTest[86411:20872420] NB -[LGPerson saySomething] - KC
複製代碼
相信看到這個結果你們都是和我同樣一臉懵逼。下面就分爲2個問題,咱們來研究一下
經過結果也看到了,這段代碼是能夠運行的。而且成功調用了[LGPerson saySomething]方法,咱們首先看爲什麼能成功調用,它和咱們註釋的普通的實例對象調用爲什麼能同樣。
首先來看一下普通方法調用的時序:
如圖所示:
接着來分析代碼中的執行時序:
id cls = [LGPerson class];
獲取到類對象指針void *obj= &cls;
獲取到指向該類對象cls
的對象obj
[(__bridge id)obj print];
消息發送按照上面的時序的結構總結一下,就是:
obj
--> 指針cls
--> [LGPerson class]類地址因此從本質上來講,最後消息發送的時候,都是根據isa
獲取到類的內存空間,而後再方法列表中查找IMP
。而obj
指向的是一個cls
,可是cls
正巧也是指向類的內存空間的,因此一樣也能夠找到方法列表中的print
方法,進行調用
經過運行結果咱們能夠看到,方法調用
self.name
居然打印出了VC
中的NSString
的臨時變量的值,那麼爲何會出現這個結果呢?
咱們知道任何OC方法的底層都是一個C函數,而且函數頭兩個參數是默認參數id self
和 SEL _cmd
,那麼self是誰呢
首先咱們仍是來看一下正常的調用self.name
是如何找到的
self
(消息接受者)指的就是咱們實例化的對象person
,而person
指向的是實例對象的內存空間首地址,而內存空間首地址是第一個元素isa
,佔用8
字節,name
是第二個元素,也是佔用8
個字節。尋找self.name
的過程就是指針偏移的過程,由於isa
佔用了8
個,因此找到name
的值時,只須要向後便宜8
個字節便可。如圖所示:那麼咱們再來看一下代碼中時如何找到self.name
的
經過上面的時序,咱們知道這二者在調用方法上是對等的。這裏消息發送時的self
(消息接受者)指的就是obj
,cls
指針至關於person
指針所指向的實例對象裏面的isa
指針,同理,此時指向的類的首地址,可是要找的是name
,向下指針偏移8個後,找到了生命的臨時變量的值,因此會打印出來
這裏就涉及到了爲何會找到臨時變量的問題,若是去掉了臨時變量,又會打印什麼呢,這裏就有了函數棧空間的做用
棧空間的做用,是用來存放被調用函數其內部所定義的局部變量的。咱們都知道棧的特色是先入後出,因此先存進棧的在底層。因爲方法的調用和super
的調用,都會產生局部變量,因此viewdidload
的棧示意圖以下:
self
,即當前的
viewcontroller
若是加了一個臨時變量NSString
,棧的結構就會變成以下所示,因此會打印它的值
能夠看出此時對象的實例變量獲取即爲void *ivar = &obj + offset(N)
可是咱們指針偏移的offset
不正確,獲取不到對應變量的首地址,此時就會出現野指針的狀況,因此千萬不能像代碼中那麼用