前言
這是三個沒多少關(guān)系的知識點,只是今天,2019年12月23號,突然就想把它們放到一起,來簡單梳理下。
消息轉(zhuǎn)發(fā)機制
參考:
iOS Runtime 消息轉(zhuǎn)發(fā)機制原理和實際用途
iOS - 動態(tài)添加方法和消息轉(zhuǎn)發(fā)
調(diào)用一個對象的方法,如果方法已經(jīng)實現(xiàn),則會接收消息并響應(yīng)。如果方法未實現(xiàn),如果沒有做預(yù)防措施,則會運行時崩潰并報錯 unrecognized selector sent to instance 0x1c4015330。這中間經(jīng)歷了什么呢?參考下面這張原理圖:

下面結(jié)合源碼分析:
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
+ (BOOL)resolveClassMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"testFuncation"]) {
// 利用runtime,動態(tài)添加該方法。
/*
* 添加處理代碼
*/
const char *classChar = [NSStringFromClass([self class]) UTF8String];
Class metaClass = objc_getClass(classChar);
IMP imp = class_getMethodImplementation(self, @selector(createClassFun));
class_addMethod(metaClass, sel, imp, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@",NSStringFromSelector(sel));
if ([NSStringFromSelector(sel) isEqualToString:@"testFuncation"]) {
// 利用runtime,動態(tài)添加該方法。方法加入本類中
/*
* 添加處理代碼
*/
IMP imp = class_getMethodImplementation(self, @selector(createTestFuncation));
class_addMethod([self class], sel, imp, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"----- forwardingTargetForSelector: %@",NSStringFromSelector(aSelector));
// 返回備用響應(yīng)對象
/*
* 其實這里就可以做一些統(tǒng)一處理了
*/
// 譬如: return [PlaceObject newMethod];
return [super forwardingTargetForSelector:aSelector];
}
/// 創(chuàng)建簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
// 如果返回 nil 則手動創(chuàng)建簽名
if ([super methodSignatureForSelector:aSelector] == nil) {
NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sign;
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 創(chuàng)建備用對象,并手動調(diào)用
SEL sel = anInvocation.selector;
Person *p = [Person new];
if ([p respondsToSelector:sel]) {
// 喚醒這個方法
[anInvocation invokeWithTarget:p];
} else {
// 最終,還是不行,就會崩潰
[self doesNotRecognizeSelector: sel];
}
}
/// 類方法
+(void)createClassFun {
NSLog(@"createClassFun 被調(diào)用");
}
/// 實例方法
-(void)createTestFuncation {
NSLog(@"createTestFuncation 被調(diào)用");
}
調(diào)用:
Person *p = [[Person alloc] init];
[p performSelector:@selector(testFuncation)];
resolveClassMethod & resolveInstanceMethod
動態(tài)添加類方法和實例方法。給對象發(fā)送消息,未找到對應(yīng)方法時首先調(diào)用的方法。在這里動態(tài)添加方法,防止崩潰。
關(guān)于動態(tài)添加方法:
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
- Class:需要添加方法的類。
- SEL:調(diào)用時未實現(xiàn)的方法。
- IMP:防止 Crash 添加的方法的指針。
- const char:方法描述:
- "v@:": 這是一個 void 類型方法,沒有參數(shù)傳入。
- "i@:": 這是一個 int 類型方法,沒有參數(shù)傳入。
- "i@:@": 這是一個 int 類型方法,有一個參數(shù)傳入。
每一個方法都會默認隱藏兩個參數(shù):self 和 _cmd。其中 self 代表方法調(diào)用者,_cmd 標識這個方法的 SEL,是用來描述這個方法的返回值、參數(shù)的。詳見官網(wǎng)。
另外,用這種方式添加的方法是無法直接調(diào)用的,要用 performSelector: 調(diào)用。因為 performSelector 方法是運行時去尋找方法的,在編譯時不做校驗,目前 Xcode 對未實現(xiàn)的方法會給出警告。 class_addMethod 是在運行時添加方法的。
forwardingTargetForSelector
方法重定向,是 NSObject 的函數(shù),用來決定誰執(zhí)行方法。
methodSignatureForSelector & forwardInvocation
-
[NSMethodSignature signatureWithObjCTypes:"v@:"];是方法簽名。參數(shù)規(guī)則同上面所說的const char。 -
forwardInvocation是一個, 不能識別的消息的分發(fā)中心,在這里對最終未響應(yīng)的消息做處理。該方法只有對象無法正常響應(yīng)消息時才會被調(diào)用。
小結(jié)
- 調(diào)用 resolveClassMethod 或者 resolveInstanceMethod,給個機會讓類添加這個方法的實現(xiàn)。
- 調(diào)用 forwardingTargetForSelector 讓別的對象去實現(xiàn)這個函數(shù)。
- 調(diào)用 methodSignatureForSelector(函數(shù)簽名) 和 forwardInvocation(函數(shù)分發(fā)執(zhí)行) 靈活地將目標函數(shù)以其他方式實現(xiàn)。
- 如果以上三步仍未解決問題,則調(diào)用 doesNotRecognizeSelector 拋出異常。
響應(yīng)者鏈和事件傳遞
參考:
響應(yīng)者
響應(yīng)者對象是 UIResponder。只有繼承 UIResponder 的類,才能處理事件。如 UIApplication、UIView 及其子類等。 CALayer 不是 UIResponder 的子類,無法處理事件。
查找響應(yīng)者、事件的分發(fā)和傳遞
- 當 iOS 程序發(fā)生觸摸事件后,系統(tǒng)會利用 RunLoop 將事件加入到 UIApplication 管理的一個任務(wù)隊列中。此時,該觸摸事件被封裝成一個 UIEvent 對象。具體可參考 深入理解RunLoop。
- UIApplication 將處于任務(wù)隊列最前端的事件向下分發(fā),即 分發(fā)給 UIWindow 處理。
- UIWindow 將事件向下分發(fā)。此時調(diào)用
hitTest:withEvent:在視圖層次結(jié)構(gòu)中找到一個合適的 UIView 來處理觸摸事件。 - UIView 首先要看自己是否能夠處理事件,觸摸點是否在自己身上。如果能。則繼續(xù)尋找子視圖。
- 遍歷子控件,重復(fù)以上兩步。
- 如果沒有找到,那么自己就是事件處理者。
- 如果自己不能處理,則不做任何處理。
UIView 不接受事件處理的情況有以下三種:
- alpha < 0.01
- userInteractionEnabled = NO
- hidden = YES
找響應(yīng)者,即是從父View 到 子View 的查找過程。主要用到了 UIView 的 hitTest:withEvent: 以及 pointInside:withEvent: 兩個方法:

// 此方法返回的View是本次點擊事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 判斷一個點是否落在范圍內(nèi)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
hitTest
點擊檢測方法,實現(xiàn)方法大致如下:
/// 查找最佳響應(yīng) UIView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判斷當前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判斷點在不在當前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.從后往前遍歷自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
// 把當前控件上的坐標系轉(zhuǎn)換成子控件上的坐標系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 尋找到最合適的view
return fitView;
}
}
// 循環(huán)結(jié)束,表示沒有比自己更合適的view
return self;
}
遍歷是從后往前的順序,即先遍歷最底部的父視圖。所以當父視圖的 userInteractionEnabled 為 NO 時,子視圖無法尋找最合適的 view,無法做出響應(yīng)。
pointInside
判斷一個點是否在觸摸范圍內(nèi)。這個方法可以用來擴展按鈕的點擊范圍:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
CGRect bounds = self.bounds;
bounds = CGRectInset(bounds, -10, -10);
// CGRectContainsPoint 判斷點是否在矩形內(nèi)
return CGRectContainsPoint(bounds, point);
}
也可以為不規(guī)則的按鈕定制點擊區(qū)域:
// // 改變圖片的點擊范圍
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// 控件范圍寬度多40,高度20
CGRect bounds = CGRectInset(self.bounds, -20, -20);
NSLog(@"point = %@",NSStringFromCGPoint(point));
UIBezierPath *path1 = [UIBezierPath bezierPathWithRect:CGRectMake(-20, 0, 40, 120)];
UIBezierPath *path2 = [UIBezierPath bezierPathWithRect:CGRectMake(self.frame.size.width - 20, 0, 40, 120)];
if (([path1 containsPoint:point] || [path2 containsPoint:point])&& CGRectContainsPoint(bounds, point)){
//如果在path區(qū)域內(nèi),返回YES
return YES;
}
return NO;
}
響應(yīng)者鏈
響應(yīng)者鏈是從最合適的 view 開始,將處理事件傳遞給下一個響應(yīng)者。響應(yīng)者鏈的傳遞方法是事件傳遞的反方法,如果所有響應(yīng)者都不處理,則事件被丟棄。通常用 UIResponder 的 nextResponder 方法來尋找上級響應(yīng)者。
下圖響應(yīng)者鏈來自 官網(wǎng):

也可以通過下面代碼查看響應(yīng)者鏈:
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *b = [UIButton new];
b.frame = CGRectMake(100, 100, 100, 100);
b.backgroundColor = [UIColor redColor];
[b addTarget:self action:@selector(clickedBtn:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:b];
}
-(void)clickedBtn: (UIButton *)btn {
UIResponder *res = btn.nextResponder;
while (res != nil) {
NSLog(@"res = %@",res);
res = res.nextResponder;
}
NSLog(@"結(jié)束");
}
用文字來表述執(zhí)行流程為:
- 如果 hitTest view 或者 first responder 不處理此事件,則將事件傳遞給其 nextResponder 處理。
- 重復(fù)步驟 1,直到找到視圖層級的頂級視圖 UIWindow對象。如果 window 仍不處理此事件,則傳遞給 UIApplication。
- 如果 UIApplication 對象不處理此事件,則事件被丟棄。
下面通過完整的圖來感受下:

App 啟動時做了什么
通過添加環(huán)境變量可以打印出 App 的啟動時間分析:
Edit scheme -> Run -> Arguments -> Environment Variables:
- 添加:DYLD_PRINT_STATISTICS,設(shè)置為 1。
- 如果需要更詳細的信息,那就添加:DYLD_PRINT_STATISTICS_DETAILS,設(shè)置為 1。
以下手動 copy 自:戴銘 iOS開發(fā)高手課 02 | App 啟動速度怎么做優(yōu)化與監(jiān)控?
這里不對優(yōu)化做過多細節(jié)分析。主要了解 APP 從點擊到渲染出來第一個頁面,中間做了什么。
App 啟動類型
一般情況下,App 的啟動分為冷啟動和熱啟動:
- 冷啟動是指,App 點擊,它的進程不在系統(tǒng)里,需要系統(tǒng)創(chuàng)建一個進程分配給它啟動的情況。這是一次完整的啟動過程。
- 熱啟動是指,App 在冷啟動后用戶將 App 退入后臺,在 App 的進程還在系統(tǒng)里的情況下,用戶重新啟動進入 App 的過程,這個過程做的事情非常少。
這里只展開講一下 App 冷啟動的優(yōu)化。
一般而言,App 的啟動時間,指的是從用戶點擊 App 開始,到用戶看到第一個界面的時間??偟脕碚f,App 的啟動主要包括三個階段:
- main() 函數(shù)執(zhí)行前;
- main() 函數(shù)執(zhí)行后;
- 首屏渲染完成后。
整個啟動過程示意圖,如下所示:

main() 函數(shù)執(zhí)行前
在 main() 函數(shù)執(zhí)行前,系統(tǒng)主要會做以下幾件事情:
- 加載可執(zhí)行文件(App 的 .o 文件的集合);
- 加載動態(tài)鏈接庫,進行 rebase 指針調(diào)整和 bind 符號綁定;
- Objc 運行時的初始處理,包括 Objc 相關(guān)類的注冊、Category 注冊、selector 唯一性檢查等;
- 初始化,包括 +load() 方法、attribute((constructor)) 修飾的函數(shù)調(diào)用、創(chuàng)建 C++ 靜態(tài)全局變量。
相應(yīng)的,這個階段對于啟動優(yōu)化來說,可以做的事情包括:
- 減少動態(tài)庫加載。每個庫本身都有依賴關(guān)系,蘋果公司建議使用更少的動態(tài)庫,并且建議在使用動態(tài)庫的數(shù)量較多時,盡量將多個動態(tài)庫進行合并。數(shù)量上,蘋果公司最多可以支持 6 個非系統(tǒng)動態(tài)庫合并為一個。
- 減少加載啟動后不會去使用的類或者方法。
- +load() 方法里的內(nèi)容可以放到首屏渲染完成后再執(zhí)行。或使用 +initialize() 方法替換掉。因為在一個 +load() 方法里,進行運行時方法替換會帶來 4 毫秒的消耗。不要小看這 4 毫秒,積少成多,執(zhí)行 +load() 方法對啟動速度的影響會越來越大。
- 控制 C++ 全局變量的數(shù)量。
main() 函數(shù)執(zhí)行后
main() 函數(shù)執(zhí)行后的階段,指的是從 main() 函數(shù)執(zhí)行開始,到 AppDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染的相關(guān)方法執(zhí)行完成。
首頁業(yè)務(wù)代碼都是要在這個階段,也就是首屏渲染前執(zhí)行的,主要包括了:
- 首屏初始化所需配置文件的讀寫操作;
- 首屏列表大數(shù)據(jù)的讀??;
- 首屏渲染的大量計算等。
很多時候,開發(fā)者會把各種初始化工作都放到這個階段執(zhí)行,導(dǎo)致渲染完成滯后。更加優(yōu)化的開發(fā)方式,應(yīng)該是從功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是 App 啟動必要的初始化功能,而哪些是只需要在對應(yīng)功能開始使用時才需要初始化的。梳理完之后,將這些初始化功能分別放到合適的階段進行。
首屏渲染完成后
首屏渲染后的這個階段,主要完成的是,非首屏其他業(yè)務(wù)服務(wù)模塊的初始化、監(jiān)聽的注冊、配置文件的讀取等。從函數(shù)上來看,這個階段指的就是截止到 didFinishLaunchingWithOptions 方法作用域內(nèi)執(zhí)行首屏渲染之后的所有方法執(zhí)行完成。簡單說的話,這個階段就是從渲染完成時開始,到 didFinishLaunchingWithOptions 方法作用域結(jié)束時結(jié)束。
這個階段用戶已經(jīng)能夠看到 App 的首頁信息了,所以優(yōu)化的優(yōu)先級排在最后。但是,那些會卡住主線程的方法還是需要最優(yōu)先處理的,不然還是會影響到用戶后面的交互操作。