什么是Runtime
- C語言是一門靜態(tài)語言李请,在編譯階段已確定所有的數(shù)據(jù)類型里烦,函數(shù)方法盯蝴。
- Objective-C是一門動態(tài)語言,在編譯時是不知道具體的變量類型率寡,函數(shù)方式,是在運行階段才確定相關(guān)類型倚搬,函數(shù)冶共。因此我們可以動態(tài)去修改相關(guān)函數(shù)調(diào)用,變量等,使OC變得更靈活捅僵。
- Objective-C的運行時機制叫做Runtime家卖。
- Runtime實際是一個庫,OC通過Runtime去調(diào)用底層C語言方法庙楚。
消息轉(zhuǎn)發(fā)機制
所有的Objective-C方法上荡,在編譯時都是轉(zhuǎn)化為對C方法objc_msgsend()的調(diào)用
1.先來看看OC正常的函數(shù)調(diào)用
先創(chuàng)建一個Test類
// .h文件
@interface Test : NSObject
- (void)run;
- (void)eatFood:(NSString *)food;
+ (void)run;
@end
//.m文件
@implementation Person
- (void)run {
NSLog(@"Run方法運行");
}
- (void)eatFood:(NSString *)food {
NSLog(@"吃%@", food);
}
+ (void)run {
NSLog(@"執(zhí)行了類方法Run");
}
@end
在viewController中調(diào)用Test.h,并在viewDidLoad執(zhí)行
#import "Test.h"
- (void)viewDidLoad {
[super viewDidLoad];
Test *obj = [Test new];
//執(zhí)行run方法
[obj run];
//執(zhí)行eat方法
[obj eatFood:@"水果"];
//執(zhí)行類方法
[Test run];
}
2.通過Runtime去調(diào)用方法
#import "Test.h"
//調(diào)用Runtime需要引入對應(yīng)庫
#import <objc/message.h>
- (void)viewDidLoad {
[super viewDidLoad];
Test *obj = [Test new];
objc_msgSend(obj, @selector(run));
objc_msgSend(obj, @selector(eatFood:), @"水果");
objc_msgSend([Test class], @selector(run));
}
objc_msgSend中可以傳入多個參數(shù)
- 第一個參數(shù)為執(zhí)行的對象(類方法,則傳類馒闷,類實際也是一種對象)
- 第二個參數(shù)為調(diào)用的方法
-
第三個及后序參數(shù)為可選參數(shù)酪捡,傳入第二個參數(shù)方法需要的參數(shù)
執(zhí)行結(jié)果如圖:
運行結(jié)果
2、注意蘋果是禁止使用objc_msgsend方法的纳账,要使用需要關(guān)閉對應(yīng)檢測Build Setting -> Enable Strict Checking of objc_msgSend Calls改為No
修改設(shè)置
交換方法
1.交換方法是我們開發(fā)中經(jīng)常運用到逛薇,在很多老得項目中,有大量使用一個老方法疏虫,如果一個又一個去修改會耗費大量的時間永罚,所以可以通過RunTime交換方法,快速替換卧秘。
2.舉個例子
#在原有方法中調(diào)用了[NSURL URLWithString:]
NSURL *url = [NSURL URLWithString:@"www.baidu.com"];
NSLog(@"%@", url);
#如果url中不包含中文呢袱,該方法正常執(zhí)行,但若插入中文翅敌,url會為空
url = [NSURL URLWithString:@"www.baidu.com\中文"];
NSLog(@"%@", url);
3.這時后就需要替換全部URLWithString方法产捞。
我們可以創(chuàng)建一個NSURL的分類,實現(xiàn)一個新的方法NewWithString
@implementation NSURL (url)
+ (instancetype)NewWithString:(NSString *)str {
NSURL *url = [NSURL URLWithString:str];
if (!url) {
NSLog(@"URL為空");
}
return url;
}
@end
4.根據(jù)load方法在加載進入OC運行時被執(zhí)行哼御,可以在load方法中實現(xiàn)函數(shù)替換
- 我們需要用到class_getClassMethod獲取到原有類方法和新的類方法(獲取實例方法則通過class_getInstanceMethod)
- 通過method_exchangeImplementations來交換兩個方法
+ (void)load {
//獲取老方法
Method oldMethod = class_getClassMethod([NSURL class], @selector(URLWithString:));
//獲取新方法
Method newMethod = class_getClassMethod([NSURL class], @selector(NewWithString:));
//交換方法
method_exchangeImplementations(oldMethod, newMethod);
}
-
注意:
如果直接執(zhí)行會陷入死循環(huán)坯临。
因為在原有方法中,各個函數(shù)的方法實現(xiàn)如圖(ps 圖有點丑恋昼,見諒):
經(jīng)過修改后變?yōu)椋?/p>
因為我們在NewWithString執(zhí)行:
NSURL *url = [NSURL URLWithString:str];
它會不斷調(diào)用NewWithString的函數(shù)實現(xiàn)看靠,導致死循環(huán)。需改成:
NSURL *url = [NSURL NewWithString:str];
執(zhí)行結(jié)果如圖:
方法懶加載
節(jié)約性能液肌,我們經(jīng)常用到屬性的懶加載挟炬,函數(shù)方法同樣可以懶加載
1.同樣使用Test.h文件,不定義任何方法
2.我們在viewController直接調(diào)用
objc_msgSend(obj, @selector(lazyMethod));
因為沒有定義方法嗦哆,函數(shù)會直接報錯谤祖。
3.實際上,在沒有找到方法時老速,會執(zhí)行對應(yīng)類的
//對應(yīng)類方法
+ (BOOL)resolveClassMethod:(SEL)sel;
//對應(yīng)實例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;
4.我們先在Test.m中定義一個需要懶加載的方法
//id self, SEL _cmd為隱式參數(shù)可不寫
int newMethod(id self, SEL _cmd) {
NSLog(@"執(zhí)行了");
return 0;
}
5.在發(fā)現(xiàn)方法為需要懶加載的方法時粥喜,將函數(shù)加載進去
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"lazyMethod"]) {
class_addMethod([Test class], sel, newMethod, "i@:");
}
return [super resolveInstanceMethod:sel];
}
class_addMethod有四個參數(shù)
1.對應(yīng)類
2.方法名
3.添加的方法
4.方法需要使用的參數(shù),newMethod的返回值int 對應(yīng)"i", id self對應(yīng)"@",SEL對應(yīng)":"具體對應(yīng)可參考文檔
執(zhí)行結(jié)果如圖:
KVO底層實現(xiàn)
kvo原理是創(chuàng)建對應(yīng)類的子類橘券,在子類中重寫set方法额湘,同時修改isa指針指向新創(chuàng)建的類卿吐。
1.創(chuàng)建一個Person類,并添加name屬性用于監(jiān)聽測試锋华。
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
2.創(chuàng)建NSObject分類嗡官,添加新的監(jiān)聽方法
@interface NSObject (KVO)
- (void)new_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
3.在ViewController添加監(jiān)聽
- (void)viewDidLoad {
[super viewDidLoad];
_p = [Person new];
NSLog(@"修改前的類%@", [_p class]);
//使用自定義KVO監(jiān)聽
[self.p new_addObserver:self forKeyPath:@"name" options:0 context:nil];
NSLog(@"修改后的類%@", [_p class]);
}
//name改變后調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"監(jiān)聽到了name改變:%@",_p.name);
}
//點擊改變值,觸發(fā)KVO
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int i = 0;
i++;
_p.name = [NSString stringWithFormat:@"%d", i];
}
4.NSObject分類實現(xiàn)
- (void)new_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
//self 被觀察者
//observer 觀察者
//1毯焕、自定義子類
//獲取self類名
NSString *oldClassName = NSStringFromClass([self class]);
//創(chuàng)建self的子類
NSString *newClassName = [@"new_" stringByAppendingString:oldClassName];
const char *newName = [newClassName UTF8String];
//動態(tài)生成類
Class newClass = objc_allocateClassPair([self class], newName, 0);
//注冊類 類加入內(nèi)存中可供調(diào)用
objc_registerClassPair(newClass);
SEL s = NSSelectorFromString([NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]]);
//2衍腥、添加set方法
class_addMethod(newClass, s, (IMP)setObject, "v:@:@");
//3、修改isa指針
object_setClass(self, newClass);
//保存觀察者對象
objc_setAssociatedObject(self, "objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
void setObject(id self, SEL _cmd, id newName){
//1.調(diào)用super的set方法
id class = [self class];
//改變self的isa指針
object_setClass(self, class_getSuperclass(class));
// objc_msgSend(self, @selector(setName:), newName);
SEL s = NSSelectorFromString([NSString stringWithFormat:@"set%@:", [objc_getAssociatedObject(self, "keyPath") capitalizedString]]);
objc_msgSend(self, s, newName);
NSLog(@"修改完畢");
//拿到觀察者
id objc = objc_getAssociatedObject(self, "objc");
//通知觀察者
objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:), objc_getAssociatedObject(self, "keyPath"),self, nil, nil);
//改回子類類型
object_setClass(self, class);
}
執(zhí)行結(jié)果如圖: