Runtime淺析

什么是Runtime

  • C語言是一門靜態(tài)語言,在編譯階段已確定所有的數(shù)據(jù)類型,函數(shù)方法。
  • Objective-C是一門動(dòng)態(tài)語言,在編譯時(shí)是不知道具體的變量類型,函數(shù)方式,是在運(yùn)行階段才確定相關(guān)類型,函數(shù)。因此我們可以動(dòng)態(tài)去修改相關(guān)函數(shù)調(diào)用,變量等,使OC變得更靈活。
  • Objective-C的運(yùn)行時(shí)機(jī)制叫做Runtime。
  • Runtime實(shí)際是一個(gè)庫,OC通過Runtime去調(diào)用底層C語言方法。

消息轉(zhuǎn)發(fā)機(jī)制

所有的Objective-C方法,在編譯時(shí)都是轉(zhuǎn)化為對(duì)C方法objc_msgsend()的調(diào)用
1.先來看看OC正常的函數(shù)調(diào)用
先創(chuàng)建一個(gè)Test類

// .h文件
@interface Test : NSObject
- (void)run;
- (void)eatFood:(NSString *)food;
+ (void)run;
@end

//.m文件
@implementation Person
- (void)run {
    NSLog(@"Run方法運(yùn)行");
}
- (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需要引入對(duì)應(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中可以傳入多個(gè)參數(shù)

  • 第一個(gè)參數(shù)為執(zhí)行的對(duì)象(類方法,則傳類,類實(shí)際也是一種對(duì)象)
  • 第二個(gè)參數(shù)為調(diào)用的方法
  • 第三個(gè)及后序參數(shù)為可選參數(shù),傳入第二個(gè)參數(shù)方法需要的參數(shù)
    執(zhí)行結(jié)果如圖:


    運(yùn)行結(jié)果

    2、注意蘋果是禁止使用objc_msgsend方法的,要使用需要關(guān)閉對(duì)應(yīng)檢測(cè)Build Setting -> Enable Strict Checking of objc_msgSend Calls改為No


    修改設(shè)置

交換方法

1.交換方法是我們開發(fā)中經(jīng)常運(yùn)用到,在很多老得項(xiàng)目中,有大量使用一個(gè)老方法,如果一個(gè)又一個(gè)去修改會(huì)耗費(fèi)大量的時(shí)間,所以可以通過RunTime交換方法,快速替換。
2.舉個(gè)例子

#在原有方法中調(diào)用了[NSURL URLWithString:]
NSURL *url = [NSURL URLWithString:@"www.baidu.com"];
NSLog(@"%@", url);
#如果url中不包含中文,該方法正常執(zhí)行,但若插入中文,url會(huì)為空
url = [NSURL URLWithString:@"www.baidu.com\中文"];
NSLog(@"%@", url);

3.這時(shí)后就需要替換全部URLWithString方法。
我們可以創(chuàng)建一個(gè)NSURL的分類,實(shí)現(xiàn)一個(gè)新的方法NewWithString

@implementation NSURL (url)
+ (instancetype)NewWithString:(NSString *)str {
    NSURL *url = [NSURL URLWithString:str];
    if (!url) {
        NSLog(@"URL為空");
    }
    return url;
}
@end

4.根據(jù)load方法在加載進(jìn)入OC運(yùn)行時(shí)被執(zhí)行,可以在load方法中實(shí)現(xiàn)函數(shù)替換

  • 我們需要用到class_getClassMethod獲取到原有類方法和新的類方法(獲取實(shí)例方法則通過class_getInstanceMethod)
  • 通過method_exchangeImplementations來交換兩個(gè)方法
+ (void)load {
    //獲取老方法
    Method oldMethod = class_getClassMethod([NSURL class], @selector(URLWithString:));
    //獲取新方法
    Method newMethod = class_getClassMethod([NSURL class], @selector(NewWithString:));
    //交換方法
    method_exchangeImplementations(oldMethod, newMethod);
}
  • 注意:
    如果直接執(zhí)行會(huì)陷入死循環(huán)。
    因?yàn)樵谠蟹椒ㄖ?,各個(gè)函數(shù)的方法實(shí)現(xiàn)如圖(ps 圖有點(diǎn)丑,見諒):



    經(jīng)過修改后變?yōu)椋?/p>


    因?yàn)槲覀冊(cè)贜ewWithString執(zhí)行:
NSURL *url = [NSURL URLWithString:str];

它會(huì)不斷調(diào)用NewWithString的函數(shù)實(shí)現(xiàn),導(dǎo)致死循環(huán)。需改成:

NSURL *url = [NSURL NewWithString:str];

執(zhí)行結(jié)果如圖:


方法懶加載

節(jié)約性能,我們經(jīng)常用到屬性的懶加載,函數(shù)方法同樣可以懶加載
1.同樣使用Test.h文件,不定義任何方法
2.我們?cè)趘iewController直接調(diào)用

objc_msgSend(obj, @selector(lazyMethod));

因?yàn)闆]有定義方法,函數(shù)會(huì)直接報(bào)錯(cuò)。
3.實(shí)際上,在沒有找到方法時(shí),會(huì)執(zhí)行對(duì)應(yīng)類的

//對(duì)應(yīng)類方法
+ (BOOL)resolveClassMethod:(SEL)sel;
//對(duì)應(yīng)實(shí)例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;

4.我們先在Test.m中定義一個(gè)需要懶加載的方法

//id self, SEL _cmd為隱式參數(shù)可不寫
int newMethod(id self, SEL _cmd) {
    NSLog(@"執(zhí)行了");
    return 0;
}

5.在發(fā)現(xiàn)方法為需要懶加載的方法時(shí),將函數(shù)加載進(jìn)去

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"lazyMethod"]) {
        class_addMethod([Test class], sel, newMethod, "i@:");
    }
    return [super resolveInstanceMethod:sel];
}

class_addMethod有四個(gè)參數(shù)
1.對(duì)應(yīng)類
2.方法名
3.添加的方法
4.方法需要使用的參數(shù),newMethod的返回值int 對(duì)應(yīng)"i", id self對(duì)應(yīng)"@",SEL對(duì)應(yīng)":"具體對(duì)應(yīng)可參考文檔
執(zhí)行結(jié)果如圖:

執(zhí)行結(jié)果

KVO底層實(shí)現(xiàn)

kvo原理是創(chuàng)建對(duì)應(yīng)類的子類,在子類中重寫set方法,同時(shí)修改isa指針指向新創(chuàng)建的類。
1.創(chuàng)建一個(gè)Person類,并添加name屬性用于監(jiān)聽測(cè)試。

@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);
}

//點(diǎn)擊改變值,觸發(fā)KVO
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    static int i = 0;
    i++;
    _p.name = [NSString stringWithFormat:@"%d", i];
}

4.NSObject分類實(shí)現(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];
    //動(dòng)態(tài)生成類
    Class newClass = objc_allocateClassPair([self class], newName, 0);
    //注冊(cè)類 類加入內(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);
    //保存觀察者對(duì)象
    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é)果如圖:


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

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