iOS:消息轉(zhuǎn)發(fā)機制、響應(yīng)者鏈、App啟動前后

前言

這是三個沒多少關(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)歷了什么呢?參考下面這張原理圖:

iOS消息轉(zhuǎn)發(fā)機制.jpeg

下面結(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

  1. [NSMethodSignature signatureWithObjCTypes:"v@:"]; 是方法簽名。參數(shù)規(guī)則同上面所說的 const char 。
  2. forwardInvocation 是一個, 不能識別的消息的分發(fā)中心,在這里對最終未響應(yīng)的消息做處理。該方法只有對象無法正常響應(yīng)消息時才會被調(diào)用。

小結(jié)

  1. 調(diào)用 resolveClassMethod 或者 resolveInstanceMethod,給個機會讓類添加這個方法的實現(xiàn)。
  2. 調(diào)用 forwardingTargetForSelector 讓別的對象去實現(xiàn)這個函數(shù)。
  3. 調(diào)用 methodSignatureForSelector(函數(shù)簽名) 和 forwardInvocation(函數(shù)分發(fā)執(zhí)行) 靈活地將目標函數(shù)以其他方式實現(xiàn)。
  4. 如果以上三步仍未解決問題,則調(diào)用 doesNotRecognizeSelector 拋出異常。

響應(yīng)者鏈和事件傳遞

參考:

iOS 響應(yīng)鏈和事件傳遞

iOS 響應(yīng)者及響應(yīng)者鏈

響應(yīng)者

響應(yīng)者對象是 UIResponder。只有繼承 UIResponder 的類,才能處理事件。如 UIApplication、UIView 及其子類等。 CALayer 不是 UIResponder 的子類,無法處理事件。

查找響應(yīng)者、事件的分發(fā)和傳遞

  1. 當 iOS 程序發(fā)生觸摸事件后,系統(tǒng)會利用 RunLoop 將事件加入到 UIApplication 管理的一個任務(wù)隊列中。此時,該觸摸事件被封裝成一個 UIEvent 對象。具體可參考 深入理解RunLoop。
  2. UIApplication 將處于任務(wù)隊列最前端的事件向下分發(fā),即 分發(fā)給 UIWindow 處理。
  3. UIWindow 將事件向下分發(fā)。此時調(diào)用 hitTest:withEvent: 在視圖層次結(jié)構(gòu)中找到一個合適的 UIView 來處理觸摸事件。
  4. UIView 首先要看自己是否能夠處理事件,觸摸點是否在自己身上。如果能。則繼續(xù)尋找子視圖。
  5. 遍歷子控件,重復(fù)以上兩步。
  6. 如果沒有找到,那么自己就是事件處理者。
  7. 如果自己不能處理,則不做任何處理。

UIView 不接受事件處理的情況有以下三種:

  • alpha < 0.01
  • userInteractionEnabled = NO
  • hidden = YES

找響應(yīng)者,即是從父View 到 子View 的查找過程。主要用到了 UIView 的 hitTest:withEvent: 以及 pointInside:withEvent: 兩個方法:

iOS查找響應(yīng)者對象.png
// 此方法返回的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)者鏈.png

也可以通過下面代碼查看響應(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í)行流程為:

  1. 如果 hitTest view 或者 first responder 不處理此事件,則將事件傳遞給其 nextResponder 處理。
  2. 重復(fù)步驟 1,直到找到視圖層級的頂級視圖 UIWindow對象。如果 window 仍不處理此事件,則傳遞給 UIApplication。
  3. 如果 UIApplication 對象不處理此事件,則事件被丟棄。

下面通過完整的圖來感受下:

iOS響應(yīng)者鏈.png

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 的啟動主要包括三個階段:

  1. main() 函數(shù)執(zhí)行前;
  2. main() 函數(shù)執(zhí)行后;
  3. 首屏渲染完成后。

整個啟動過程示意圖,如下所示:

APP啟動過程示意圖.png

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)先處理的,不然還是會影響到用戶后面的交互操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容