一、Runtime原理
Runtime是iOS核心運(yùn)行機(jī)制之一震捣,iOS App加載庫荔棉、加載類闹炉、執(zhí)行方法調(diào)用,全靠Runtime润樱,這一塊的知識個人認(rèn)為是最基礎(chǔ)的渣触,基本面試必問。
1壹若、Runtime消息發(fā)送機(jī)制
1)iOS調(diào)用一個方法時嗅钻,實際上會調(diào)用objc_msgSend(receiver, selector, arg1, arg2, ...),該方法第一個參數(shù)是消息接收者店展,第二個參數(shù)是方法名养篓,剩下的參數(shù)是方法參數(shù);
2)iOS調(diào)用一個方法時赂蕴,會先去該類的方法緩存列表里面查找是否有該方法柳弄,如果有直接調(diào)用,否則走第3)步概说;
3)去該類的方法列表里面找碧注,找到直接調(diào)用,把方法加入緩存列表糖赔;否則走第4)步萍丐;
4)沿著該類的繼承鏈繼續(xù)查找,找到直接調(diào)用放典,把方法加入緩存列表逝变;否則消息轉(zhuǎn)發(fā)流程;
很多面試者大體知道這個流程刻撒,但是有關(guān)細(xì)節(jié)不是特別清楚骨田。
- 問他/她objc_msgSend第一個參數(shù)、第二個參數(shù)声怔、剩下的參數(shù)分別代表什么,不知道舱呻;
- 很多人只知道去方法列表里面查找醋火,不知道還有個方法緩存列表。
通過這些細(xì)節(jié)箱吕,可以了解一個人是否真正掌握了原理芥驳,而不是死記硬背。
2茬高、Runtime消息轉(zhuǎn)發(fā)機(jī)制
如果在消息發(fā)送階段沒有找到方法兆旬,iOS會走消息轉(zhuǎn)發(fā)流程,流程圖如下所示:
1)動態(tài)消息解析怎栽。檢查是否重寫了resolveInstanceMethod 方法丽猬,如果返回YES則可以通過class_addMethod 動態(tài)添加方法來處理消息宿饱,否則走第2)步;
2)消息target轉(zhuǎn)發(fā)脚祟。forwardingTargetForSelector 用于指定哪個對象來響應(yīng)消息谬以。如果返回nil 則走第3)步;
3)消息轉(zhuǎn)發(fā)由桌。這步調(diào)用 methodSignatureForSelector 進(jìn)行方法簽名为黎,這可以將函數(shù)的參數(shù)類型和返回值封裝。如果返回 nil 執(zhí)行第四步行您;否則返回 methodSignature铭乾,則進(jìn)入 forwardInvocation ,在這里可以修改實現(xiàn)方法娃循,修改響應(yīng)對象等炕檩,如果方法調(diào)用成功,則結(jié)束淮野。否則執(zhí)行第4)步捧书;
4)報錯 unrecognized selector sent to instance。
很多人知道這四步骤星,但是筆者一般會問:
- 怎么在項目里全局解決"unrecognized selector sent to instance"這類crash经瓷?本人發(fā)現(xiàn)很多人回答不出來,說明面試者肯定是在死記硬背洞难,你都知道因為消息轉(zhuǎn)發(fā)那三步都沒處理才會報錯舆吮,為什么不知道在消息轉(zhuǎn)發(fā)里面處理呢?
- 如果面試者知道可以在消息轉(zhuǎn)發(fā)里面處理队贱,防止崩潰色冀,再問下面試者,你項目中是在哪一步處理的柱嫌,看看其是否有真正實踐過锋恬?
二、load與initialize
1编丘、load與initialize調(diào)用時機(jī)
+load在main函數(shù)之前被Runtime調(diào)用与学,+initialize 方法是在類或它的子類收到第一條消息之前被調(diào)用的,這里所指的消息包括實例方法和類方法的調(diào)用嘉抓。
2索守、load與initialize在分類、繼承鏈的調(diào)用順序
- load方法的調(diào)用順序為:
子類的 +load 方法會在它的所有父類的 +load 方法之后執(zhí)行抑片,而分類的 +load 方法會在它的主類的 +load 方法之后執(zhí)行卵佛。
如果子類沒有實現(xiàn) +load 方法,那么當(dāng)它被加載時 runtime 是不會去調(diào)用父類的 +load 方法的。同理截汪,當(dāng)一個類和它的分類都實現(xiàn)了 +load 方法時疾牲,兩個方法都會被調(diào)用。 - initialize的調(diào)用順序為:
+initialize 方法的調(diào)用與普通方法的調(diào)用是一樣的挫鸽,走的都是消息發(fā)送的流程说敏。如果子類沒有實現(xiàn) +initialize 方法,那么繼承自父類的實現(xiàn)會被調(diào)用丢郊;如果一個類的分類實現(xiàn)了 +initialize 方法盔沫,那么就會對這個類中的實現(xiàn)造成覆蓋。 - 怎么確保在load和initialize的調(diào)用只執(zhí)行一次
由于load和initialize可能會調(diào)用多次枫匾,所以在這兩個方法里面做的初始化操作需要保證只初始化一次架诞,用dispatch_once來控制
筆者在面試過程中發(fā)現(xiàn)很多人對于load與initialize在分類、繼承鏈的調(diào)用順序不清楚干茉。對怎么保證初始化安全也不清楚
三谴忧、RunLoop原理
RunLoop蘋果原理圖
圖中展現(xiàn)了 Runloop 在線程中的作用:從 input source 和 timer source 接受事件,然后在線程中處理事件角虫。
1沾谓、RunLoop與線程關(guān)系
- 一個線程是有一個RunLoop還是多個RunLoop? 一個戳鹅;
- 怎么啟動RunLoop均驶?主線程的RunLoop自動就開啟了,子線程的RunLoop通過Run方法啟動枫虏。
2妇穴、Input Source 和 Timer Source
兩個都是 Runloop 事件的來源,其中 Input Source 又可以分為三類
- Port-Based Sources隶债,系統(tǒng)底層的 Port 事件腾它,例如 CFSocketRef ,在應(yīng)用層基本用不到;
- Custom Input Sources死讹,用戶手動創(chuàng)建的 Source;
- Cocoa Perform Selector Sources瞒滴, Cocoa 提供的 performSelector 系列方法,也是一種事件源;
Timer Source指定時器事件赞警,該事件的優(yōu)先級是最低的逛腿。
本人一般會問定時器事件的優(yōu)先級是怎么樣的,大部分人回答不出來仅颇。
3、解決NSTimer事件在列表滾動時不執(zhí)行問題
因為定時器默認(rèn)是運(yùn)行在NSDefaultRunLoopMode碘举,在列表滾動時候忘瓦,主線程會切換到UITrackingRunLoopMode,導(dǎo)致定時器回調(diào)得不到執(zhí)行。
有兩種解決方案:
- 指定NSTimer運(yùn)行于 NSRunLoopCommonModes下耕皮。
- 在子線程創(chuàng)建和處理Timer事件境蜕,然后在主線程更新 UI。
四凌停、事件分發(fā)機(jī)制及響應(yīng)者鏈
1粱年、事件分發(fā)機(jī)制
iOS 檢測到手指觸摸 (Touch) 操作時會將其打包成一個 UIEvent 對象,并放入當(dāng)前活動Application的事件隊列罚拟,UIApplication 會從事件隊列中取出觸摸事件并傳遞給單例的 UIWindow 來處理台诗,UIWindow 對象首先會使用 hitTest:withEvent:方法尋找此次Touch操作初始點所在的視圖(View),即需要將觸摸事件傳遞給其處理的視圖赐俗,這個過程稱之為 hit-test view拉队。
hitTest:withEvent:方法的處理流程如下:
- 首先調(diào)用當(dāng)前視圖的 pointInside:withEvent: 方法判斷觸摸點是否在當(dāng)前視圖內(nèi);
- 若返回 NO, 則 hitTest:withEvent: 返回 nil阻逮,若返回 YES, 則向當(dāng)前視圖的所有子視圖 (subviews) 發(fā)送 hitTest:withEvent: 消息粱快,所有子視圖的遍歷順序是從最頂層視圖一直到到最底層視圖(后加入的先遍歷),直到有子視圖返回非空對象或者全部子視圖遍歷完畢叔扼;
- 若第一次有子視圖返回非空對象事哭,則 hitTest:withEvent: 方法返回此對象,處理結(jié)束瓜富;
-
如所有子視圖都返回空鳍咱,則 hitTest:withEvent: 方法返回自身 (self)。
流程圖如下:
image
2食呻、響應(yīng)者鏈原理
iOS的事件分發(fā)機(jī)制是為了找到第一響應(yīng)者流炕,事件的處理機(jī)制叫做響應(yīng)者鏈原理。
所有事件響應(yīng)的類都是 UIResponder 的子類仅胞,響應(yīng)者鏈?zhǔn)且粋€由不同對象組成的層次結(jié)構(gòu)每辟,其中的每個對象將依次獲得響應(yīng)事件消息的機(jī)會。當(dāng)發(fā)生事件時干旧,事件首先被發(fā)送給第一響應(yīng)者渠欺,第一響應(yīng)者往往是事件發(fā)生的視圖,也就是用戶觸摸屏幕的地方椎眯。事件將沿著響應(yīng)者鏈一直向下傳遞挠将,直到被接受并做出處理。一般來說编整,第一響應(yīng)者是個視圖對象或者其子類對象舔稀,當(dāng)其被觸摸后事件被交由它處理,如果它不處理掌测,就傳遞給它的父視圖(superview)對象(如果存在)處理内贮,如果沒有父視圖,事件就會被傳遞給它的視圖控制器對象 ViewController(如果存在),接下來會沿著頂層視圖(top view)到窗口(UIWindow 對象)再到程序(UIApplication 對象)夜郁。如果整個過程都沒有響應(yīng)這個事件什燕,該事件就被丟棄。一般情況下竞端,在響應(yīng)者鏈中只要由對象處理事件屎即,事件就停止傳遞。
一個典型的事件響應(yīng)路線如下:
First Responser --> 父視圖-->The Window --> The Application --> nil(丟棄)
我們可以通過 [responder nextResponder] 找到當(dāng)前 responder 的下一個 responder事富,持續(xù)這個過程到最后會找到 UIApplication 對象技俐。
五、內(nèi)存泄露檢測與循環(huán)引用
1赵颅、造成內(nèi)存泄露原因
- 在用C/C++時虽另,創(chuàng)建對象后未銷毀,比如調(diào)用malloc后不free饺谬、調(diào)用new后不delete捂刺;
- 調(diào)用CoreFoundation里面的C方法后創(chuàng)建對對象后不釋放。比如調(diào)用CGImageCreate不調(diào)用CGImageRelease募寨;
- 循環(huán)引用族展。當(dāng)對象A和對象B互相持有的時候,就會產(chǎn)生循環(huán)引用拔鹰。常見產(chǎn)生循環(huán)引用的場景有在VC的cellForRowAtIndexPath方法中cell block引用self仪缸。
2、常見循環(huán)引用及解決方案
1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用屬性造成循環(huán)引用列肢。
cell.clickBlock = ^{
self.name = @"akon";
};
cell.clickBlock = ^{
_name = @"akon";
};
解決方案:把self改成weakSelf恰画;
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.name = @"akon";
};
2)在cell的block中直接引用VC的成員變量造成循環(huán)引用。
//假設(shè) _age為VC的成員變量
@interface TestVC(){
int _age;
}
cell.clickBlock = ^{
_age = 18;
};
解決方案有兩種:
- 用weak-strong dance
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf->age = 18;
};
- 把成員變量改成屬性
//假設(shè) _age為VC的成員變量
@interface TestVC()
@property(nonatomic, assign)int age;
@end
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.age = 18;
};
3)delegate屬性聲明為strong瓷马,造成循環(huán)引用拴还。
@interface TestView : UIView
@property(nonatomic, strong)id<TestViewDelegate> delegate;
@end
@interface TestVC()<TestViewDelegate>
@property (nonatomic, strong)TestView* testView;
@end
testView.delegate = self; //造成循環(huán)引用
解決方案:delegate聲明為weak
@interface TestView : UIView
@property(nonatomic, weak)id<TestViewDelegate> delegate;
@end
4)在block里面調(diào)用super,造成循環(huán)引用欧聘。
cell.clickBlock = ^{
[super goback]; //造成循環(huán)應(yīng)用
};
解決方案片林,封裝goback調(diào)用
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
[weakSelf _callSuperBack];
};
- (void) _callSuperBack{
[self goback];
}
5)block聲明為strong
解決方案:聲明為copy
6)NSTimer使用后不invalidate造成循環(huán)引用。
解決方案:
- NSTimer用完后invalidate怀骤;
- NSTimer分類封裝
+ (NSTimer *)ak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)(void))block
repeats:(BOOL)repeats{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ak_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)ak_blockInvoke:(NSTimer*)timer{
void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}
--
- 用YYWeakProxy來創(chuàng)建定時器
3费封、怎么檢測循環(huán)引用
- 靜態(tài)代碼分析。 通過Xcode->Product->Anaylze分析結(jié)果來處理蒋伦;
- 動態(tài)分析弓摘。用MLeaksFinder(只能檢測OC泄露)或者Instrument或者OOMDetector(能檢測OC與C++泄露)。
六痕届、VC生命周期
考察viewDidLoad衣盾、viewWillAppear寺旺、ViewDidAppear等方法的執(zhí)行順序。
假設(shè)現(xiàn)在有一個 AViewController(簡稱 Avc) 和 BViewController (簡稱 Bvc)势决,通過 navigationController 的push 實現(xiàn) Avc 到 Bvc 的跳轉(zhuǎn),調(diào)用順序如下:
1蓝撇、A viewDidLoad
2果复、A viewWillAppear
3、A viewDidAppear
4渤昌、B viewDidLoad
5虽抄、A viewWillDisappear
6、B viewWillAppear
7独柑、A viewDidDisappear
8迈窟、B viewDidAppear
如果再從 Bvc 跳回 Avc,調(diào)用順序如下:
1忌栅、B viewWillDisappear
2车酣、A viewWillAppear
3、B viewDidDisappear
4索绪、A viewDidAppear
資料推薦
如果你正在跳槽或者正準(zhǔn)備跳槽不妨動動小手湖员,添加一下咱們的交流群931542608來獲取一份詳細(xì)的大廠面試資料為你的跳槽多添一份保障。