【Objective-c】 runtime_又是一黑魔法

如果不裝X,跟咸魚又有什么區(qū)別了纸巷。
聽了一節(jié)關于runtime相關的課程,這里第一時間做個筆記眶痰,方便自己過后的復習瘤旨。


一、 OC 消息運行時機制

場景1:創(chuàng)建一個繼承于NSObject的Person類竖伯,添加一個對象方法-(void)eat存哲,當每次調(diào)用這個對象方法時打印一行文字。如下圖:

// .h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)eat;
@end
// .m文件
#import "Person.h"
@implementation Person
- (void)eat
{
    NSLog(@"我要食大柴飯");
}
@end

正常是導入Person的頭文件七婴,創(chuàng)建對象宏胯,直接調(diào)用對象方法.

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc]init];
    [p eat];
}

@end

上面的方法,會OC的人都不是什么問題本姥,接下來慢慢的揭開消息機制的神秘面紗。

    調(diào)用對象方法
    /*
        方式一:
     */
    [p eat];
    
    /*
        方式二:
     */
    [p performSelector:@selector(eat)];
    
    /*
        方式三:
        消息發(fā)送(注意要先導入 <objc/message.h> 頭文件)
        從Xcode 5.0 開始蘋果就不建議使用低層方式
     */
    objc_msgSend(p, @selector(eat));

補充使用message.h頭文件需要以下操作:

Paste_Image.png

前面的只是前菜杭棵,接下來是要剝析Person對象的實例另一種方法

//未剝析前
Person *p = [[Person alloc]init];

//1.0
Person *p = [Person alloc];
p = [p init];

//2.0 特別注明一點
//objc_msgSend(<#id self#>, <#SEL op, ...#>)
//第一個參數(shù)是id類型婚惫,如果是對象就直接傳對象就可以,
//如果是類魂爪,則傳類(好吧先舷!忽略我說的)
Person *p = objc_msgSend([Person class], @selector(alloc));
p = objc_msgSend(p, @selector(init));

//3.0 這里還有一點不足就是還是引用了Person類
// 不急我們接下來會一步步的解決
//首先獲取"類"的方法有如下三種
//第一種:[Person class]
//第二種:NSClassFromString(@"Person") 
//第三種:objc_getRequiredClass("Person")
//3.1
Person *p = objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc));
//3.2 根據(jù)面向?qū)ο蠖鄳B(tài)的特性:Person類繼承于NSObject類
NSObject *p = objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc));

//4.0 上面已經(jīng)轉變完成了,都是通過調(diào)用函數(shù)滓侍,沒有用到OC語法
//  再將上面的兩行代碼寫在一塊了蒋川,這個給人的等級就是上天了
NSObject *p = objc_msgSend( objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc)), @selector(init));

換個寫法,是不是有點feel了撩笆,接下來驗證我們的結論
創(chuàng)建一個新工程(只有main函數(shù)的工程)

Paste_Image.png
Paste_Image.png
Paste_Image.png
Paste_Image.png
Paste_Image.png

Paste_Image.png
二捺球、 runtime
2.1 runtime簡歷

runtime 是蘋果提供的一套低層的API

runtime的作用有下面三個:
a、動態(tài)的創(chuàng)建一個類
b夕冲、動態(tài)的修改一個類的屬性/方法
c氮兵、遍歷一個類的所有的成員變量

runtime使用前提
導入同文件<objc/message.h>或者<objc/runtime.h>
備注:<objc/message.h>頭文件已經(jīng)包括了<objc/runtime.h>

兩個概念
Method : 代表成員方法
Ivar : 成員變量

2.2 runtime使用

runtime的作用:
1、獲取類方法或?qū)ο蠓椒?br> 2歹鱼、交互方法的實現(xiàn)

場景2: 通過URLWithString:方法創(chuàng)建一個NSURL對象泣栈,如"www.baidu.com",一般情況是無BUG的弥姻,但是如果此時將url改成"www.baidu.com/好好學習",則創(chuàng)建的NSURL為nil南片,就會導致以后的代碼出現(xiàn)BUG,甚至崩潰庭敦,問題是就算是做了一個全局的斷點也找不到這個BUG疼进。
所以我們的需求是:NSURL 這個類的URLWithString:方法添加一個功能!在創(chuàng)建URL的同時也能判斷是否為空

為一個類添加新的方法或者是修改原有的方法螺捐,就有創(chuàng)建類別和繼承兩種方式颠悬。

繼承
現(xiàn)在這種場景矮燎,如果是使用繼承創(chuàng)建一個新的NSURL子類的話是能修改這個隱藏的BUG,不過就要將項目中的NSURL替換成繼承的子類赔癌,在一個已經(jīng)持續(xù)開發(fā)幾個月的項目诞外,這種方案并不是最可行的。

類別
類別灾票,就是一個補丁峡谊,為一個原有類的基礎增加新功能。但是類別也是有缺點的
第一:不能添加屬性(意思是添加的屬性不會自動生成setter方法和getter方法)刊苍;
第二:不建議重寫類的方法(原因請看下面的例子)既们。
當然這兩個問題還是有辦法解決的,使用我們今天的主角"runtime"正什,runtime先生有兩個特殊的技能:1啥纸、動態(tài)添加成員變量(屬性);2婴氮、交換方法的實現(xiàn)

//重寫類方法1:會造成死循環(huán)
+ (instancetype)URLWithString:(NSString *)URLString
{
    NSURL *url = [NSURL URLWithString:URLString];
}

//重寫類方法2:不能實現(xiàn)
+ (instancetype)URLWithString:(NSString *)URLString
{
  //因為NSURL的父類是NSObject斯棒,并沒有URLWithString:方法
    NSURL *url = [super URLWithString:URLString];
}

現(xiàn)在的解決思路:創(chuàng)建NSURL分類(NSURL+url),并添加一個新的實現(xiàn)方法主经,使用runtime與原來的URLWithString:交互實現(xiàn)方法荣暮,詳情看代碼

//NSURL(url).h 文件
#import <Foundation/Foundation.h>
@interface NSURL (url)
+ (instancetype)MZ_URLWithString:(NSString *)URLString;
@end
//NSURL(url).m 文件
#import "NSURL+url.h"
#import <objc/runtime.h>
@implementation NSURL (url)

+ (instancetype)MZ_URLWithString:(NSString *)URLString
{
    NSURL *url = [NSURL URLWithString:URLString];
    if (url == nil) {
        NSLog(@"URL 為空");
    }
    return url;
}
@end

上面是創(chuàng)建的NSURL類別,新增的一個MZ_URLWithString:方法罩驻,但是離我們目標還有一步:交互方法實現(xiàn)

//NSURL(url).m 文件
#import "NSURL+url.h"
#import <objc/runtime.h>
@implementation NSURL (url)

+ (void)load
{
    NSLog(@"NSURL(url).m文件加載了");
    //交換方法實現(xiàn)
    //第一步:獲取這兩個方法
    //class_getClassMethod 獲取類方法
    //class_getInstanceMethod 獲取對象方法
    Method URLWithStr = class_getClassMethod(self, @selector(URLWithString:));
    Method MRURLWithStr = class_getClassMethod(self, @selector(MZ_URLWithString:));
    //交換方法
    method_exchangeImplementations(URLWithStr, MRURLWithStr);
}

+ (instancetype)MZ_URLWithString:(NSString *)URLString
{
    //注意:這里不能調(diào)用原來的URLWithString:方法穗酥,否則會造成死循環(huán)崩潰的
    NSURL *url = [NSURL MZ_URLWithString:URLString];
    if (url == nil) {
        NSLog(@"URL 為空");
    }
    return url;
}
@end

到這里就已經(jīng)是完成了,在load方法中交互方法的實現(xiàn)有個好處惠遏,就是不用在是使用這個補丁的類中添加"NSURL+url.h"頭文件砾跃,會直接生效。(備注:利跟弊都是相對的节吮。既然能在不導入頭文件的情況就能全局的替換原有方法的實現(xiàn)蜓席,容易產(chǎn)生混淆,并且不像繼承那樣可以直接跳轉代碼查看實現(xiàn)课锌,所以推薦厨内,不對,是必須添加注釋渺贤,添加注釋雏胃,添加注釋)
你一定會疑惑為什么在load方法中交互方法的實現(xiàn),就會全局有效志鞍?
小編也是一知半解的瞭亮,如果想深入了解可以參考下面兩篇文章:
TerryZhang:iOS的load方法與initialize方法
Draveness:你真的了解load方法么?

關于+(void)load方法至少要記坠膛铩:
只要類的實現(xiàn)文件引入項目中(如下圖),在程序啟動時就會執(zhí)行统翩,并且是執(zhí)行一次仙蚜。

Paste_Image.png

runtime的作用 2
動態(tài)創(chuàng)建屬性

場景3:

runtime的作用 3
動態(tài)創(chuàng)建方法

場景4:Person類聲明了一個對象如-(void)eat,但該方法無實現(xiàn)方法厂汗。使用runtime為Person類動態(tài)創(chuàng)建一個對象方法的實現(xiàn)

viewController.m 文件

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
@property (nonatomic,strong) Person *p;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.p = [[Person alloc]init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //調(diào)用不帶參數(shù)的方法
    [self.p performSelector:@selector(eat)];   
}
Person.m 文件

#import "Person.h"
#import <objc/runtime.h>

@implementation Person

//函數(shù)的名字就是函數(shù)的指針
//每一個函數(shù)內(nèi)都默認有這兩個參數(shù)委粉,這是隱式參數(shù),這是系統(tǒng)傳過來的
void eat(id self,SEL _cmd){
    NSLog(@"調(diào)用了%@的%@方法",self,NSStringFromSelector(_cmd));
}


/*
    當調(diào)用沒有實現(xiàn)的對象方法娶桦,會先調(diào)用此方法
    // 當調(diào)用沒實現(xiàn)的類方法贾节,會調(diào)用此方法 + (BOOL)resolveClassMethod:(SEL)sel
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    //動態(tài)添加方法
    if (sel == @selector(eat)) {
        /**
            1.類類型
            2.方法編號 (即方法名)
            3.方法實現(xiàn),函數(shù)指針
            4.返回值類型(c語言字符串)
         */
        class_addMethod([Person class], sel,(IMP)eat, nil);
    }
    
    return [super resolveInstanceMethod:sel];
}
@end

到這里動態(tài)創(chuàng)建一個方法是已經(jīng)完成了衷畦,這個時候再調(diào)用Person類的eat方法就不會出現(xiàn)崩潰的情況了栗涂。

這里有3個知識點:

1、當調(diào)用沒有實現(xiàn)的方法(對象方法或類方法)祈争,系統(tǒng)崩潰前會先調(diào)用+(BOOL)resolveInstanceMethod:或+(BOOL)resolveClassMethod:方法

2斤程、函數(shù)的名字就是函數(shù)的指針,雖然這是C語言的基礎菩混,但也要提一下(小編自己也是忘了)暖释。并且每個函數(shù)都有兩個默認的隱式參數(shù)(id self,SEL _cmd),"self"代表當前對象,"_cmd"代表方法名墨吓。提醒:這兩個參數(shù)是系統(tǒng)提供的值,系統(tǒng)提供的值纹磺,系統(tǒng)提供的值帖烘,重要的事說三遍。

3橄杨、runtime動態(tài)創(chuàng)建『方法』的方法:class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)秘症,這個方法有四個參數(shù),前三個就不提了式矫,代碼塊上都有標注乡摹,重點是第四個參數(shù)(c語言字符串),是關于這個方法返回值和入?yún)㈩愋偷囊粋€編碼字符串采转。含義如下:

未標題-1.png

以void eat(id self,SEL _cmd)函數(shù)為例子聪廉,第四個參數(shù)字符串的值應該是"v@:"。
當然這里我們列舉的是無參的案列故慈,如果是有帶參數(shù)該怎么辦了板熊?
比如吃東西這個方法一個食物的名稱

//帶參數(shù)
void eatOBJ(id self,SEL _cmd,id obj){
    NSLog(@"我吃了%@",obj);
}

/*
    當調(diào)用沒有實現(xiàn)的方法,會先調(diào)用此方法
    //+ (BOOL)resolveClassMethod:(SEL)sel
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    //動態(tài)添加方法
    if (sel == @selector(eat)) {
        /**
            1.類類型
            2.方法編號 (即方法名)
            3.方法實現(xiàn)察绷,函數(shù)指針
            4.返回值類型(c語言字符串)
         */
        class_addMethod([Person class], sel,(IMP)eat, nil);
        
    } else if (sel == @selector(eat:)){
        class_addMethod([Person class], sel, (IMP)eatOBJ, "v@:@");
    }
    return [super resolveInstanceMethod:sel];
}

    //調(diào)用帶參數(shù)的方法
    [self.p performSelector:@selector(eatOBJ:) withObject:@"紅燒肉"];

這里就不解釋了

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末干签,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拆撼,更是在濱河造成了極大的恐慌容劳,老刑警劉巖喘沿,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異竭贩,居然都是意外死亡蚜印,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門娶视,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晒哄,“玉大人,你說我怎么就攤上這事〉粒” “怎么了洋满?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長较木。 經(jīng)常有香客問我,道長青柄,這世上最難降的妖魔是什么伐债? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮致开,結果婚禮上峰锁,老公的妹妹穿的比我還像新娘。我一直安慰自己双戳,他們只是感情好虹蒋,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著飒货,像睡著了一般魄衅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上塘辅,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天晃虫,我揣著相機與錄音,去河邊找鬼扣墩。 笑死哲银,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的呻惕。 我是一名探鬼主播盘榨,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蟆融!你這毒婦竟也來了草巡?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎山憨,沒想到半個月后查乒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡郁竟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年玛迄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棚亩。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蓖议,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出讥蟆,到底是詐尸還是另有隱情勒虾,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布瘸彤,位于F島的核電站修然,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏质况。R本人自食惡果不足惜愕宋,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望结榄。 院中可真熱鬧中贝,春花似錦、人聲如沸臼朗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽依溯。三九已至,卻和暖如春瘟则,著一層夾襖步出監(jiān)牢的瞬間黎炉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工醋拧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留慷嗜,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓丹壕,卻偏偏與公主長得像庆械,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子菌赖,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容