如果不裝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頭文件需要以下操作:
前面的只是前菜杭棵,接下來是要剝析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ù)的工程)
二捺球、 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í)行一次仙蚜。
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)㈩愋偷囊粋€編碼字符串采转。含義如下:
以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:@"紅燒肉"];
這里就不解釋了