關於runtime的那些事
runtime 簡介
- runtime簡稱運行時,程序在運行過程中都會轉成runtime的C代碼執行。
- OC中的一切都被設計成對象,實際上類的本質也是一個對象,在runtime中用結構體表示。
- 對於oc來說,在編譯時並不能決定真正調用哪個函數,只有在真正運行時纔會根據函數名找到對應的函數來調用。
- runtime在編譯階段可以調用任何的函數,即使這個函數沒有實現,只要聲明過就不會報錯。
runtime 應用
1.交換方法
#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.攔截調用
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
說明:- 第一個方法是當你調用一個不存在的類方法的時候,會調用此方法,默認返回NO,你可以加上自己的實現後返回YES。
- 第二個方法是當你調用一個不存在的實例方法時會調用,與方法一類似。
- 第三個方法是將你調用的不存在的方法重定向到一個其他聲明瞭這個方法的類,只需要你返回一個有這個方法的target。
- 最後一個方法是將你調用的不存在的方法打包成NSInvocation傳給你。做完你自己的處理後,調用invokeWithTarget:方法讓某個target觸發這個方法。
[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.添加方法中示例相似@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.添加方法
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.字典轉模型
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];