Runtime就是運(yùn)行時(shí)动漾,也就是系統(tǒng)在運(yùn)行過程中的一些機(jī)制怎燥。Objective-C就是運(yùn)行時(shí)語言。
對(duì)于C語言來說堤魁,函數(shù)的調(diào)用時(shí)在編譯階段就決定了的喂链。而對(duì)于OC來說,函數(shù)的調(diào)用是動(dòng)態(tài)的妥泉,在編譯的時(shí)候并不能真正決定調(diào)用哪個(gè)函數(shù)椭微。所以,在編譯階段盲链,OC可以調(diào)用任何函數(shù)蝇率,只要函數(shù)有聲明即可,而C則不然刽沾,如果函數(shù)只有聲明而沒有實(shí)現(xiàn)本慕,則會(huì)報(bào)錯(cuò)。
消息機(jī)制
runtime中最重要的就是消息機(jī)制侧漓。OC中任何方法的調(diào)用锅尘,其本質(zhì)都是通過runtime去發(fā)送消息,也就是說布蔗,OC底層是通過runtime去實(shí)現(xiàn)的藤违。
//創(chuàng)建一個(gè)NSObject類型的對(duì)象obj
NSObject *obj = [[NSObject alloc] init];
//為了方便說明浪腐,我們將該行代碼的兩步拆分開來,即
id obj = [NSObject alloc]; //分配內(nèi)存
obj = [obj init]; //初始化
假設(shè)這兩行代碼所在文件為XXX.m顿乒,使用終端打開文件目錄议街,輸入命令 clang -rewrite-objc XXX.m ,運(yùn)行后發(fā)現(xiàn)文件所在目錄下新增了一個(gè)名為“XXX.cpp”的C++文件淆游,打開該文件傍睹,我們可以搜索到上面兩行代碼的底層實(shí)現(xiàn)如下:
id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
obj = ((id (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("init"));
去掉括號(hào)中的強(qiáng)轉(zhuǎn)和無用代碼,我們便對(duì)這兩行代碼完成了“瘦身”:
//id obj = [NSObject alloc];
id obj = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc"));
//obj = [obj init];
obj = objc_msgSend(obj, sel_registerName("init"));
我們發(fā)現(xiàn)每個(gè)方法的底層都是通過發(fā)送消息——objc_msgSend(); 犹菱。
事實(shí)上拾稳,我們確實(shí)也可以在XXX.m文件中用這兩行代碼代替原有的兩行代碼。
由于XCode6之后腊脱,蘋果不再鼓勵(lì)開發(fā)人員使用runtime編寫代碼访得,所以直接使用會(huì)報(bào)錯(cuò)。我們可以找到工程文件->BuildSetting->搜索msg->Enable Strict Checking of objc_msgSend Calls陕凹,將默認(rèn)的YES改為No即可悍抑。
然后在我們需要用到runtime的文件中導(dǎo)入頭文件#import<objc/message.h>。
上面“瘦身”后的代碼仍然過于復(fù)雜杜耙,實(shí)際用起來很不方便搜骡。下面我們對(duì)函數(shù)objc_msgSend()作一下說明,加入上層的東西佑女,進(jìn)一步精簡代碼记靡,方便開發(fā)中使用。
//id 消息由誰發(fā)送
//SEL 發(fā)送什么消息
objc_msgSend(<#id self#>, <#SEL op, ...#>)
所以团驱,我們對(duì)原有的代碼簡化如下:
//id obj = [NSObject alloc];
id obj = objc_msgSend([NSObject class], @selector(alloc));
//obj = [obj init];
obj = objc_msgSend(obj, @selector(init));
涉及到需要參數(shù)的函數(shù)摸吠,用runtime實(shí)現(xiàn)的方式是類似的。
在Dog類中有一個(gè)函數(shù)
- (void)bark:(int)times
{
NSLog(@"叫了%d聲",times);
}
其他文件中嚎花,可以作如下調(diào)用:
Dog *littleDog = [[Dog alloc] init];
objc_msgSend(littleDog, @selector(bark:), 3);
需要指出的是寸痢,使用runtime可以調(diào)用一個(gè)類的私有方法,及時(shí)Dog.h文件中沒有聲明bark:方法紊选,依然可以使用runtime調(diào)用啼止。
使用Runtime交換方法
場(chǎng)景:想要在原有項(xiàng)目代碼中,對(duì)系統(tǒng)類UIImage中的方法-imageNamed:添加“判斷圖片是否成功加載”的功能兵罢。由于原有代碼中很可能多次用到過UIImage的這個(gè)方法族壳,所以自定義方法或者重寫+imageNamed:等常規(guī)思路去實(shí)現(xiàn)會(huì)很難或者很麻煩。
想要修改系統(tǒng)的方法實(shí)現(xiàn)趣些,給系統(tǒng)方法添加新的功能仿荆,我們可以利用runtime進(jìn)行方法交換。
以該場(chǎng)景舉例,創(chuàng)建一個(gè)UIImage的分類拢操,在該分類中完成如下操作即可锦亦。
#import "UIImage+LFImage.h"
#import <objc/message.h>
@implementation UIImage (LFImage)
//+load方法在將類加載進(jìn)內(nèi)存的時(shí)候調(diào)用,并且只會(huì)調(diào)用一次
//所以我們?cè)谠摲椒ㄖ羞M(jìn)行兩個(gè)方法的交換
+(void)load
{
//獲取+imageNamed:方法
Method method1 = class_getClassMethod(self, @selector(imageNamed:));
//獲取+lf_imageNamed:方法
Method method2 = class_getClassMethod(self, @selector(lf_imageNamed:));
//交換方法
method_exchangeImplementations(method1, method2);
//完成這步之后令境,調(diào)用+iamgeNamed:實(shí)際調(diào)用的是+lf_imageNamed:杠园,反之亦然
}
+(UIImage *)lf_imageNamed:(NSString *)name
{
//加載圖片
UIImage *image = [UIImage lf_imageNamed:name];
//由于方法已交換,所以此時(shí)如果寫imageNamed: 舔庶,會(huì)造成死循環(huán)
//判斷是否加載成功
if (image)
NSLog(@"加載成功");
else
NSLog(@"加載失敗");
return image;
}
@end
這樣抛蚁,我們就完成了 +imageNamed: 和 lf_imageNamed: 的交換,項(xiàng)目中所有 +imageNamed: 的調(diào)用惕橙,其實(shí)都是調(diào)用的lf_imageNamed: 方法瞧甩。
動(dòng)態(tài)添加方法
動(dòng)態(tài)添加方法在實(shí)際開發(fā)中并不太常用,此處做簡要說明弥鹦。
創(chuàng)建一個(gè)對(duì)象dog肚逸,并利用performSelector:調(diào)用Dog類中既沒有聲明有咩有實(shí)現(xiàn)的方法run。
Dog *dog = [[Dog alloc] init];
[dog performSelector:@selector(run)];
需要注意的是彬坏,由于Dog類中沒有聲明方法run朦促,所以直接[Dog run]編譯器會(huì)報(bào)錯(cuò)。
由于沒有實(shí)現(xiàn)方法run栓始,所以在程序運(yùn)行的時(shí)候仍然會(huì)報(bào)錯(cuò)务冕。我們需要在Dog類中做如下處理。
#import "Dog.h"
#import <objc/message.h>
@implementation Dog
//當(dāng)一個(gè)對(duì)象調(diào)用了未實(shí)現(xiàn)的方法時(shí)幻赚,會(huì)調(diào)用該方法
//該方法用于動(dòng)態(tài)添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"%@", NSStringFromSelector(sel));
//判斷是否為run
if (sel == NSSelectorFromString(@"run")) {
//__unsafe_unretained Class cls : 給哪個(gè)類添加方法
//SEL name : 什么方法
//IMP imp : 方法實(shí)現(xiàn)--方法名
//const char *types : 方法類型
class_addMethod(self, sel, (IMP)rrrun, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
//當(dāng)一個(gè)類調(diào)用了未實(shí)現(xiàn)的方法
//+ (BOOL)resolveClassMethod:(SEL)sel
//{
//
//}
//此處不會(huì)生成方法列表
void rrrun(id self, SEL _cmd) {
NSLog(@"跑啊跑");
}
@end
動(dòng)態(tài)添加屬性
利用runtime動(dòng)態(tài)添加屬性禀忆,在實(shí)際開發(fā)中相對(duì)是比較常用的。自定義的類坯屿,我們可以自由設(shè)置屬性,所以此處主要作用于系統(tǒng)的類巍扛。動(dòng)態(tài)添加屬性的本質(zhì)领跛,就是讓某個(gè)屬性與對(duì)象產(chǎn)生一個(gè)關(guān)聯(lián)。
例如撤奸,我們對(duì)NSObject添加一個(gè)name屬性吠昭。
NSObject *obj = [[NSObject alloc] init];
obj.name = @"對(duì)象"; //由于NSObject類沒有name屬性,顯然此處會(huì)報(bào)錯(cuò)
這里給NSObject添加一個(gè)分類NSObject+Name胧瓜,然后在分類中處理如下矢棚。
#import <Foundation/Foundation.h>
@interface NSObject (Name)
@property NSString *name;
//由于在分類中,@property只會(huì)生成getter府喳、setter的聲明蒲肋,所以沒有必要對(duì)屬性進(jìn)行修飾
@end
#import "NSObject+Name.h"
#import <objc/message.h>
@implementation NSObject (Name)
- (void)setName:(NSString *)name
{
//讓name屬性與當(dāng)前對(duì)象產(chǎn)生關(guān)聯(lián)
//object 需要添加屬性的對(duì)象
//key 屬性名稱
//value 值
//policy 保存策略
/*
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
*/
objc_setAssociatedObject(self, @"name", @"對(duì)象", OBJC_ASSOCIATION_COPY);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, @"name");
}
@end