iOS 關於runtime的那些事

關於runtime的那些事

runtime 簡介

  • runtime簡稱運行時,程序在運行過程中都會轉成runtime的C代碼執行。
  • OC中的一切都被設計成對象,實際上類的本質也是一個對象,在runtime中用結構體表示。
  • 對於oc來說,在編譯時並不能決定真正調用哪個函數,只有在真正運行時纔會根據函數名找到對應的函數來調用。
  • runtime在編譯階段可以調用任何的函數,即使這個函數沒有實現,只要聲明過就不會報錯。

runtime 應用


1.交換方法


使用場景:通常我們如果系統自帶的方法所完成的功能不能滿足我們的需求時,我們可以用runtime的方法的交換給系統的方法擴展一些功能,並且保持原有的功能。(其實繼承系統類重新方法也可達到同樣的效果)

注意事項:在給分類添加方法時,除非你明白你要實現什麼樣的功能(如將字典或者數組中的文字打印出來,而不是亂碼),否則不能重寫系統方法,因爲這樣會覆蓋系統方法的實現。

代碼示例:我們要實現這樣的一個功能。當app有點卡的情況下,如果我們多次點擊一個按鈕,就會出現多次跳轉到同一個頁面的bug,影響了用戶的體驗。爲了解決這一問題,我們使用runtime交換方法的功能,替換UIButton響應事件,完成根據響應時間間隔來判斷是否執行消息發送。
#import <UIKit/UIKit.h>

@interface UIButton (Custom)
/* 響應的時間間隔 */
@property (assign, nonatomic) NSTimeInterval acceptEventInterval;
@end

#import "UIButton+Custom.h"
#import <objc/runtime.h>

@interface UIButton ()
@property (assign, nonatomic) NSTimeInterval custom_acceptEventTime;
@end

@implementation UIButton (Custom)

+ (void)load {
    
    SEL sysSEL = @selector(sendAction:to:forEvent:);

    Method systemMethod = class_getInstanceMethod(self, sysSEL);
    
    SEL customSEL = @selector(custom_sendAction:to:forEvent:);

    Method customMethod = class_getInstanceMethod(self, customSEL);
    
    // 如果不存在該方法的實現,就替換系統的方法,否則就交換兩個方法的實現
    
    BOOL didAddMethod = class_addMethod(self, sysSEL, method_getImplementation(customMethod), method_getTypeEncoding(customMethod));
    
    if (didAddMethod) {
        class_replaceMethod(self, customSEL, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
    } else {
        method_exchangeImplementations(systemMethod, customMethod);
    }
    
}

- (void)custom_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
       
    //  當前時間的時間戳
    NSTimeInterval currentTimeInterval = [[NSDate date] timeIntervalSince1970];
    
    BOOL needSendAction = (currentTimeInterval - self.custom_acceptEventTime) >= self.acceptEventInterval;
    
    // 更新上次點擊的時間戳
    if (self.acceptEventInterval > 0) {
        self.custom_acceptEventTime = currentTimeInterval;
    }
    
    if (needSendAction) {
        //  此處其實調用的是系統方法 sendAction:to:forEvent:,並不存在死循環。
        [self custom_sendAction:action to:target forEvent:event];
    } else {
        NSLog(@"點擊過於頻繁,請稍候重試!");
    }
}

#pragma mark - Setter & Getter

const NSString *kAcceptEventIntervalKey = @"acceptEventIntervalKey";

- (NSTimeInterval)acceptEventInterval {
    return [objc_getAssociatedObject(self, &kAcceptEventIntervalKey) doubleValue];
}

- (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval {
    objc_setAssociatedObject(self, &kAcceptEventIntervalKey, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

const NSString *kAcceptEventTimeKey = @"acceptEventTimeKey";

- (NSTimeInterval)custom_acceptEventTime {
    return [objc_getAssociatedObject(self, &kAcceptEventTimeKey) doubleValue];
}

- (void)setCustom_acceptEventTime:(NSTimeInterval)custom_acceptEventTime {
    objc_setAssociatedObject(self, &kAcceptEventTimeKey, @(custom_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

2.攔截調用


應用場景:在程序運行過程中,如果沒有找到相應方法的實現,就會轉向攔截調用。通俗點講,再找不到調用方法,程序崩潰之前,我們有機會重新NSObject的四個方法來攔截處理。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
說明:
  • 第一個方法是當你調用一個不存在的類方法的時候,會調用此方法,默認返回NO,你可以加上自己的實現後返回YES。
  • 第二個方法是當你調用一個不存在的實例方法時會調用,與方法一類似。
  • 第三個方法是將你調用的不存在的方法重定向到一個其他聲明瞭這個方法的類,只需要你返回一個有這個方法的target。
  • 最後一個方法是將你調用的不存在的方法打包成NSInvocation傳給你。做完你自己的處理後,調用invokeWithTarget:方法讓某個target觸發這個方法。
代碼示例:

1.對象在接收到無法響應的消息後,首先會調用所屬類的 +(BOOL)resolveInstanceMethod:(SEL)sel 方法
[self performSelector:@selector(doSomething)];
void dynamicMethod(id self, SEL _cmd) {
    NSLog(@"doSomething %@",NSStringFromSelector(_cmd));
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 消息轉發
    if (sel == @selector(doSomething)) {
        NSLog(@"動態添加方法");
        class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
上面與3.添加方法中示例相似

2.如果在+(BOOL)resolveInstanceMethod:(SEL)sel 中沒有找到或者添加方法,消息會繼續往下傳遞,會調用方法- (id)forwardingTargetForSelector:(SEL)aSelector,看看有沒有對象可以執行這個方法,我們在第一個控制器FirstViewController中注掉上述+(BOOL)resolveInstanceMethod:(SEL)sel 方法的實現。在控制器SecondViewController中添加一個方法:
@implementation SecondViewController
- (void)myMethod {
    NSLog(@"%@",NSStringFromClass([self class]));
}

重點來了,現在我想在FirstViewController調用SecondViewController中的- (void)myMethod這個方法,因爲這兩個控制器並無繼承關係,按照正常的邏輯肯定是不會執行的,因此而程序崩潰。現在我們來處理一下消息的轉發,在FirstViewController添加如下:
    [self performSelector:@selector(myMethod)];
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        Class class = NSClassFromString(@"SecondViewController");
        UIViewController *viewController = [[class alloc] init];
        if (aSelector == @selector(myMethod)) {
            return viewController;
        }
        return nil;
    }

這樣我們就在FirstViewController調用SecondViewController中的- (void)myMethod這個方法,消息轉發成功,同時,也相當於完成了一個多繼承。

3.添加方法


使用場景:加載一個類到內存,需要給每個方法生成映射表,這樣是比較耗費資源的。我們可以使用runtime動態爲某個類添加方法。
代碼示例:
person沒有cry方法,但是可以通過performSelector:調用,如果就這樣運行我們的程序,必然會出現錯誤 -[SFPerson cry]: unrecognized selector sent to instance 0x7fd4b1425a70
SFPerson *person = [[SFPerson alloc] init];
[person performSelector:@selector(cry)];
#import "SFPerson.h"
#import <objc/runtime.h>

@implementation SFPerson

// 默認方法都有兩個隱式參數
void cry(id self, SEL sel) {
    NSLog(@"%@, %@",self, NSStringFromSelector(sel));
}

// 一個對象調用未實現的方法,會調用resolveInstanceMethod:進行處理,並且會把對象的方法列表傳過來
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(cry)) {
        // 參數說明
        // 給哪個類添加方法,這裏我們寫成self
        // 添加的方法名稱,本例是重寫的攔截調用傳過來的selector
        // 添加方法的函數實現(函數地址),C方法的實現可以直接獲得。如果是OC方法,可以用+(IMP)instanceMethodForSelector:(SEL)aSelector方法獲得
        // 函數的類型(返回值+參數類型),v:void @:對象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)cry, "v@:");
	return YES;
    }
    return [super resolveInstanceMethod:sel];
    
}
@end

4.關聯對象


應用場景:當我們想爲現有類添加一個屬性時,如果繼承得到一個子類,那麼就顯得有點麻煩。這時候我們可爲其分類添加一個屬性(除非萬不得已,否則不建議在分類中添加屬性,儘量在本類中添加),本質是給這個類添加屬性上的關聯。
在交換方法代碼示例中已有體現,此處不再贅述。

5.字典轉模型


應用場景:我們從網絡或者服務器拿到數據後(一般來說,會返回一個字典),需要封裝成對應的模型,供我們方便實用。那麼我們就可根據字典中的key自動生成對應的屬性。
代碼示例:
    const char kPropertiesKey = '\0';
    // 判斷是否存在關聯對象
    NSArray *propertyLists = objc_getAssociatedObject(self, &kPropertiesKey);
    
    if (propertyLists) {
        return propertyLists;
    }
    
    // 獲取本類的屬性
    unsigned int propertyCount = 0;// 屬性的計數指針
    
    // 獲取所有屬性,並存儲到數組中
    
    objc_property_t *propertys = class_copyPropertyList([self class], &propertyCount);
    
    NSMutableArray *allNames = [NSMutableArray arrayWithCapacity:propertyCount];
    
    for (unsigned int i = 0;  i < propertyCount; i++) {
        // 獲取到屬性
        objc_property_t property = propertys[i];
        // 獲取到屬性的名稱
        const char *propertyName = property_getName(property);
        
        [allNames addObject:[NSString stringWithUTF8String:propertyName]];
    }
    
    free(propertys);
    
    objc_setAssociatedObject(self, &kPropertiesKey, allNames, OBJC_ASSOCIATION_COPY_NONATOMIC);
    
    return [allNames copy];


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章