Runtime 是一套 C 語言的 API,屬于OC 底層的實現(xiàn)。接下來將從消息機制、歸檔、Hook(下鉤子)、動態(tài)的添加方法四個方面來簡單地聊一下。
一、消息機制
OC 是一門動態(tài)語言,所有的方法調(diào)用都會在底層轉(zhuǎn)化成消息發(fā)送。為了證明這個觀點,做如下實驗。
打開Xcode,新建一個CommandLine 程序,新建一個繼承自NSObject 的Person 類,如下所示
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];//堆區(qū)分配空間 malloc
p = [p init];//初始化對象
}
return 0;
}
接下來,看一下這段代碼的底層的實現(xiàn)情況。打開終端,cd 到工程的根目錄,運行命令 :
clang -rewrite-objc main.m
會得到一個 main.cpp的文件。利用Xcode 打開,會發(fā)現(xiàn)其內(nèi)容比較長(有近10萬行代碼)。選取最后面的十幾行代碼:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("init"));
objc_msgSend(p, sel_registerName("init"));
}
return 0;
}
由上可以得知:
[Person alloc] 被轉(zhuǎn)化為了 objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
[p init] 被轉(zhuǎn)化為了 objc_msgSend(p, sel_registerName("init"))。
其中,objc_msgSend( )函數(shù)的作用是向?qū)ο蟀l(fā)送消息,objc_getClass( )作用是獲取類對象,sel_registerName( )作用是注冊消息。
假設(shè)Person對象含有一個對象方法:eat,則可以有如下幾種方式實現(xiàn)調(diào)用:
1. [p eat];
2. [p performSelector:@selector(eat)];
3. objc_msgSend(p, @selector(eat));
4. objc_msgSend(p, sel_registerName("eat"));
二、歸檔
還是以剛才的Person類為例,假設(shè)其有兩個屬性:name 和 age
@interface Person : NSObject<NSCoding>
@property(nonatomic,strong) NSString *name;
@property(nonatomic,assign) NSInteger age;
@end
其實現(xiàn)歸檔的方式為:
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
_name = [coder decodeObjectForKey:@"name"];
_age = [coder decodeIntegerForKey:@"age"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:_name forKey:@"name"];
[coder encodeInteger:_age forKey:@"age"];
}
當(dāng)類的屬性比較多的情況下,再使用這種方式歸檔的話,就不免有些麻煩,尤其是增加或者刪除某些屬性后都需要修改這兩個方法的內(nèi)容。而通過Runtime實現(xiàn)歸檔就可以避免這些麻煩的出現(xiàn):
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([self class], &count);
for(int i =0 ;i<count;i++)
{
NSString *key =[NSString stringWithUTF8String:ivar_getName(ivars[i])];
id value = [coder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);//釋放對應(yīng)的區(qū)域
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
unsigned int count = 0;
//在C里面,但凡讓傳遞一個基本數(shù)據(jù)類型的指針,它內(nèi)部就是想要改變外面的值
Ivar * ivars = class_copyIvarList([self class], &count);
for(int i =0;i<count;i++)
{
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
[coder encodeObject:[self valueForKey:key] forKey:key];
}
free(ivars);
}
Ivar 是表示成員變量的類型,class_copyIvarList( )作用是獲取成員變量的列表。注:記得引入頭文件 #import <objc/runtime.h>
三、Hook(下鉤子)
設(shè)想如下的場景:
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:@"www.baidu.com/中文"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSLog(@"%@",request);
}
我們知道,利用含有中文的字符串生成的URL對象會為 nil,而系統(tǒng)的URLWithString方法并沒有對出現(xiàn)nil 時的情況進(jìn)行提示。但我們可以自己加上,而且很容易想到通過類別的方式添加:
@interface NSURL (url)
+ (instancetype) createUrlWithString:(NSString *)str;
@end
+ (instancetype)createUrlWithString:(NSString *)str
{
NSURL *url = [NSURL URLWithString:str];
if(url == nil)
{
NSLog(@"URL 為空??!");
}
return url;
}
這樣,調(diào)用剛才寫好的createUrlWithString:方法生成URL對象就能及時察覺空的URL對象的出現(xiàn)。新的問題又出現(xiàn)了:如果這個類別是后期創(chuàng)建的,那么程序中但凡用到URLWithString:方法的地方就必須都改成createUrlWithString:方法。不但麻煩,而且容易遺漏。所以我們不禁會想:能不能不改變調(diào)用的方法,即還是調(diào)用URLWithString:方法,同時能夠達(dá)到調(diào)用createUrlWithString:的效果呢?
答案是肯定的??!
@interface NSURL (url)
+ (instancetype) createUrlWithString:(NSString *)str;
@end
+ (void)load
{
//下鉤子
Method UrlWithStr = class_getClassMethod(self, @selector(URLWithString:));
Method CreateUrlwithStr = class_getClassMethod(self, @selector(createUrlWithString:));
//交換方法實現(xiàn)??!
method_exchangeImplementations(UrlWithStr, CreateUrlwithStr);
}
//高級用法,記得寫上備注
+ (instancetype)createUrlWithString:(NSString *)str
{
NSURL *url = [NSURL createUrlWithString:str];
if(url == nil)
{
NSLog(@"URL 為空?。?);
}
return url;
}
load 方法是在APP被裝載進(jìn)內(nèi)存的時候調(diào)用,可以說比 main.m 中的 main 函數(shù)執(zhí)行的還要早。所以我們可以在 load 方法里做文章。
通過 class_getClassMethod( )方法分別獲取到 URLWithString:方法和createUrlWithString:方法的IMP( 函數(shù)指針)。這里有個形象的比喻幫助大家理解方法跟IMP的關(guān)系:方法如同書中的目錄,IMP如同書中的頁碼。方法的最終實現(xiàn)情況取決于IMP。
由此,我們可以想到,在方法不變的情況下,可以修改其相應(yīng)的IMP達(dá)到修改方法實際調(diào)用情況的目的。method_exchangeImplementations( , )函數(shù)恰好幫我們實現(xiàn)了這個目標(biāo)。即完成了URLWithString:方法和createUrlWithString:方法的IMP的交換。所以,接下來調(diào)用URLWithString:方法實際上最終調(diào)用的是我們之前已經(jīng)寫好的createUrlWithString:方法的實現(xiàn)。
細(xì)心的小伙伴可能發(fā)現(xiàn)了我這里有個細(xì)節(jié),看上去不符合常規(guī):

這里可以負(fù)責(zé)人的告訴小伙伴:沒錯!就該這樣寫??!哈哈……
因為我們已經(jīng)交換了兩個方法的IMP,因此調(diào)用createUrlWithString:方法實際上調(diào)用的是URLWithString:方法的實現(xiàn),沒毛??!如果函數(shù)里面調(diào)用的還是URLWithString:方法的話,會導(dǎo)致createUrlWithString:方法形成遞歸,即不斷地調(diào)用自己,程序會卡住,一段時間后就會閃退!感興趣的小伙伴可以試一下,下面附上我的運行結(jié)果:

四、動態(tài)地添加方法
創(chuàng)建一個類Person_add (為了區(qū)分之前的Person)繼承自NSObject,不添加任何成員方法和變量。作如下處理:
- (void)viewDidLoad {
[super viewDidLoad];
Person_add *p = [[Person_add alloc]init];
objc_msgSend(p, @selector(eat:),@"漢堡",@"水果");
}
程序運行后,大家很容易想到程序會閃退。因為Person_add 對象既沒有聲明更沒有實現(xiàn)eat:方法。
前面已經(jīng)提到,OC中所有的方法調(diào)用最終都會在底層轉(zhuǎn)化為消息發(fā)送。這里要清楚一點,objc_msgSend ( )方法看起來好像返回了數(shù)據(jù),其實objc_msgSend() 從不返回數(shù)據(jù),而是你的方法在運行時實現(xiàn)被調(diào)用后才會返回數(shù)據(jù)。下面詳細(xì)敘述消息的發(fā)送步驟:
- 首先檢測這個 selector 是不是要忽略。比如Mac OSX開發(fā),有了垃圾回收就不理會 retain,release 這些函數(shù);
- 檢測這個 selector 的target 是不是 nil,Objc 允許我們對一個 nil 對象執(zhí)行任何方法不會Crash,因為運行時會被忽略掉;
- 如果上面兩步都通過了,那么就開始查找這個類的實現(xiàn) IMP,先從 cache 里查找,如果找到了就運行對應(yīng)的函數(shù)去執(zhí)行相應(yīng)的代碼;
- 如果類的列表中找不到,就到父類的方法列表中查找,一直找到 NSObject 類為止;
- 如果還找不到,就要開始進(jìn)入動態(tài)方法解析了。
開始了動態(tài)解析后,Runtime 會調(diào)用 resolveInstanceMethod:或者 resolveClassMethod:來給我們一次動態(tài)添加方法實現(xiàn)的機會:
#import "Person_add.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation Person_add
//對象方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"%@",NSStringFromSelector(sel));
//添加一個
if(sel == @selector(eat:)){
/*
1.cls 目標(biāo)類
2.SEL 方法編號
3.IMP 方法的實現(xiàn)
4.返回值類型
*/
class_addMethod(self, sel, (IMP)haha, "V@:@");
}
return [super resolveInstanceMethod:sel];
}
/*
1.方法的調(diào)用者
2.方法的編號
*/
void haha(id self,SEL _cmd,NSString *str,NSString *str2)
{
NSLog(@"吃%@",str2);
}
@end
上面的例子為 eat:方法添加了實現(xiàn)內(nèi)容,就是 haha 方法中的代碼。其中 "V@:@" 表示返回值和參數(shù)。運行結(jié)果為:
