聊一聊 Runtime

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ī):


image.png

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


image.png

四、動態(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ā)送步驟:

  1. 首先檢測這個 selector 是不是要忽略。比如Mac OSX開發(fā),有了垃圾回收就不理會 retain,release 這些函數(shù);
  2. 檢測這個 selector 的target 是不是 nil,Objc 允許我們對一個 nil 對象執(zhí)行任何方法不會Crash,因為運行時會被忽略掉;
  3. 如果上面兩步都通過了,那么就開始查找這個類的實現(xiàn) IMP,先從 cache 里查找,如果找到了就運行對應(yīng)的函數(shù)去執(zhí)行相應(yīng)的代碼;
  4. 如果類的列表中找不到,就到父類的方法列表中查找,一直找到 NSObject 類為止;
  5. 如果還找不到,就要開始進(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é)果為:


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

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,032評論 0 9
  • 對于從事 iOS 開發(fā)人員來說,所有的人都會答出【runtime 是運行時】什么情況下用runtime?大部分人能...
    夢夜繁星閱讀 3,799評論 7 64
  • 前言 runtime其實在我們?nèi)粘i_發(fā)過程中很少使用到,尤其是像我現(xiàn)在比較初級的程序猿就更用不到了。但是去面試很多...
    WolfTin閱讀 840評論 0 2
  • 重生之愛 (文/亦濃) 過了午夜 是另一天了 可是, 竟如此不舍 紀(jì)念這個日子 十月十五號,八月二十六日 虔誠叩拜...
    開在夜里的花兒閱讀 279評論 8 10
  • JavaScript prototype每個函數(shù)都有一個prototype屬性,這個屬性是指向一個對象的引用,這個...
    MakingChoice閱讀 496評論 0 1

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