為什么其他語(yǔ)言里叫函數(shù)調(diào)用, objective c里則是給對(duì)象發(fā)消息(或者談下對(duì)runtime的理解)

C語(yǔ)言:調(diào)用函數(shù)的語(yǔ)言在聲明完函數(shù)后,如果沒有實(shí)現(xiàn)函數(shù),程序是無法編譯通過的属提。
OC:程序是可以編譯通過的,但是會(huì)有一個(gè)黃色的警告权逗。只有當(dāng)程序運(yùn)行之后才會(huì)出現(xiàn)如下的崩潰信息:

+[Function max::]: unrecognized selector sent to class 0x1000010f8*** 
Terminating app due to uncaught exception 'NSInvalidArgumentException'...

消息傳遞和調(diào)用函數(shù)對(duì)于程序員來說最大的區(qū)別就在于源代碼編譯的過程中是否能夠編譯通過.
解釋消息傳遞機(jī)制的原理就要用到OC語(yǔ)言中的運(yùn)行時(shí)系統(tǒng)(Runtime)了.

什么是Runtime

runtime是一個(gè)c和匯編寫的動(dòng)態(tài)庫(kù),它就像一個(gè)小小的系統(tǒng),將OC和C緊密關(guān)聯(lián)斟薇。這個(gè)系統(tǒng)主要做兩件事 :
1火惊、封裝C語(yǔ)言的結(jié)構(gòu)體和函數(shù),讓開發(fā)者在運(yùn)行時(shí)創(chuàng)建奔垦、檢查或者修改類、對(duì)象和方法等等尸疆。
2椿猎、傳遞消息,找出方法的最終執(zhí)行代碼寿弱。

那么我們可以利用這些方法干點(diǎn)什么犯眠?

1、遍歷對(duì)象的屬性
2症革、動(dòng)態(tài)添加/修改屬性筐咧,動(dòng)態(tài)添加/修改/替換方法
3、動(dòng)態(tài)創(chuàng)建類/對(duì)象/協(xié)議等等
4噪矛、方法攔截調(diào)用

運(yùn)行時(shí)系統(tǒng)是一個(gè)提供一系列公開函數(shù)接口以及數(shù)據(jù)結(jié)構(gòu)的動(dòng)態(tài)鏈接庫(kù)量蕊,這些頭文件位于/usr/include/objc。許多這些函數(shù)允許你使用純C語(yǔ)言重寫當(dāng)你寫OC代碼后編譯器做的事情艇挨。其他形式的接口則是通過NSObject類中定義的一些方法残炮。這些方法是可以用來實(shí)現(xiàn)其他的運(yùn)行時(shí)接口來提高運(yùn)行效率。但是重寫運(yùn)行時(shí)的代碼對(duì)于使用OC語(yǔ)言進(jìn)行編程并非是必須的缩滨,但是势就,少數(shù)的運(yùn)行時(shí)函數(shù)在一些特殊情況下,對(duì)于OC程序還是很有用途的脉漏。

我們寫的代碼在程序運(yùn)行過程中都會(huì)被轉(zhuǎn)化成runtime的C代碼執(zhí)行苞冯,例如[target doSomething];會(huì)被轉(zhuǎn)化成objc_msgSend(target, @selector(doSomething));
OC中一切都被設(shè)計(jì)成了對(duì)象侧巨,我們都知道一個(gè)類被初始化成一個(gè)實(shí)例舅锄,這個(gè)實(shí)例是一個(gè)對(duì)象。實(shí)際上一個(gè)類本質(zhì)上也是一個(gè)對(duì)象刃泡,在runtime中用結(jié)構(gòu)體表示巧娱。

如果消息的接收者能夠找到對(duì)應(yīng)的selector,那么就相當(dāng)于直接執(zhí)行了接收者這個(gè)對(duì)象的特定方法烘贴;否則禁添,消息要么被轉(zhuǎn)發(fā),或是臨時(shí)向接收者動(dòng)態(tài)添加這個(gè)selector對(duì)應(yīng)的實(shí)現(xiàn)內(nèi)容桨踪,要么就干脆玩完崩潰掉老翘。

現(xiàn)在可以看出[receiver message]真的不是一個(gè)簡(jiǎn)簡(jiǎn)單單的方法調(diào)用。因?yàn)檫@只是在編譯階段確定了要向接收者發(fā)送message這條消息,而receive將要如何響應(yīng)這條消息铺峭,那就要看運(yùn)行時(shí)發(fā)生的情況來決定了墓怀。

Objective-C 的 Runtime 鑄就了它動(dòng)態(tài)語(yǔ)言的特性。Objc Runtime使得C具有了面向?qū)ο竽芰ξ兰诔绦蜻\(yùn)行時(shí)創(chuàng)建傀履,檢查,修改類莉炉、對(duì)象和它們的方法钓账。可以使用runtime的一系列方法實(shí)現(xiàn)絮宁。

相關(guān)的定義:

/// 描述類中的一個(gè)方法
typedef struct objc_method *Method;
/// 實(shí)例變量
typedef struct objc_ivar *Ivar;
/// 類別Category
typedef struct objc_category *Category;
/// 類中聲明的屬性
typedef struct objc_property *objc_property_t;

類在runtime中的表示

//類在runtime中的表示
structobjc_class {   
    Class isa;//指針梆暮,顧名思義,表示是一個(gè)什么绍昂,實(shí)例的isa指向類對(duì)象啦粹,類對(duì)象的isa指向元類
#if!__OBJC2__
    Class super_class;//指向父類
    const char *name;//類名
    long version;
    long info;
    long instance_size
    struct objc_ivar_list *ivars//成員變量列表
    struct objc_method_list **methodLists;//方法列表
    struct objc_cache *cache;//緩存: 一種優(yōu)化,調(diào)用過的方法存入緩存列表窘游,下次調(diào)用先找緩存
    struct objc_protocol_list *protocols//協(xié)議列表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

向object發(fā)送消息時(shí)唠椭,Runtime庫(kù)會(huì)根據(jù)object的isa指針找到這個(gè)實(shí)例object所屬于的類,然后在類的方法列表以及父類方法列表尋找對(duì)應(yīng)的方法運(yùn)行忍饰。id是一個(gè)objc_object結(jié)構(gòu)類型的指針泪蔫,這個(gè)類型的對(duì)象能夠轉(zhuǎn)換成任何一種對(duì)象。

然后再來看看消息發(fā)送的函數(shù):objc_msgSend函數(shù)

在引言中已經(jīng)對(duì)objc_msgSend進(jìn)行了一點(diǎn)介紹喘批,看起來像是objc_msgSend返回了數(shù)據(jù)撩荣,其實(shí)objc_msgSend從不返回?cái)?shù)據(jù)而是你的方法被調(diào)用后返回了數(shù)據(jù)。下面詳細(xì)敘述下消息發(fā)送步驟:

檢測(cè)這個(gè) selector 是不是要忽略的饶深。比如 Mac OS X 開發(fā)餐曹,有了垃圾回收就不理會(huì) retain,release 這些函數(shù)了。
檢測(cè)這個(gè) target 是不是 nil 對(duì)象敌厘。ObjC 的特性是允許對(duì)一個(gè) nil 對(duì)象執(zhí)行任何一個(gè)方法不會(huì) Crash台猴,因?yàn)闀?huì)被忽略掉。
如果上面兩個(gè)都過了俱两,那就開始查找這個(gè)類的 IMP饱狂,先從 cache 里面找,完了找得到就跳到對(duì)應(yīng)的函數(shù)去執(zhí)行宪彩。
如果 cache 找不到就找一下方法分發(fā)表休讳。
如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找尿孔,直到找到NSObject類為止俊柔。
如果還找不到就要開始進(jìn)入動(dòng)態(tài)方法解析了筹麸,后面會(huì)提到。

方法調(diào)用

讓我們看一下方法調(diào)用在運(yùn)行時(shí)的過程
如果用實(shí)例對(duì)象調(diào)用實(shí)例方法雏婶,會(huì)到實(shí)例的isa指針指向的對(duì)象(也就是類對(duì)象)操作物赶。
如果調(diào)用的是類方法,就會(huì)到類對(duì)象的isa指針指向的對(duì)象(也就是元類對(duì)象)中操作留晚。

  1. 首先酵紫,在相應(yīng)操作的對(duì)象中的緩存方法列表中找調(diào)用的方法,如果找到错维,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)并執(zhí)行憨闰。
  2. 如果沒找到,在相應(yīng)操作的對(duì)象中的方法列表中找調(diào)用的方法需五,如果找到,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)執(zhí)行
  3. 如果沒找到轧坎,去父類指針?biāo)赶虻膶?duì)象中執(zhí)行1宏邮,2.
  4. 以此類推,如果一直到根類還沒找到缸血,轉(zhuǎn)向攔截調(diào)用蜜氨。
  5. 如果沒有重寫攔截調(diào)用的方法,程序報(bào)錯(cuò)捎泻。

以上的過程給我?guī)淼膯l(fā):

  1. 重寫父類的方法飒炎,并沒有覆蓋掉父類的方法,只是在當(dāng)前類對(duì)象中找到了這個(gè)方法后就不會(huì)再去父類中找了笆豁。
  2. 如果想調(diào)用已經(jīng)重寫過的方法的父類的實(shí)現(xiàn)郎汪,只需使用super這個(gè)編譯器標(biāo)識(shí),它會(huì)在運(yùn)行時(shí)跳過在當(dāng)前的類對(duì)象中尋找方法的過程闯狱。

攔截調(diào)用

在方法調(diào)用中說到了煞赢,如果沒有找到方法就會(huì)轉(zhuǎn)向攔截調(diào)用。
那么什么是攔截調(diào)用呢哄孤。
攔截調(diào)用就是照筑,在找不到調(diào)用的方法程序崩潰之前,你有機(jī)會(huì)通過重寫NSObject的四個(gè)方法來處理瘦陈。

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后兩個(gè)方法需要轉(zhuǎn)發(fā)到其他的類處理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation*)anInvocation;
  • 第一個(gè)方法是當(dāng)你調(diào)用一個(gè)不存在的類方法的時(shí)候凝危,會(huì)調(diào)用這個(gè)方法,默認(rèn)返回NO晨逝,你可以加上自己的處理然后返回YES蛾默。
  • 第二個(gè)方法和第一個(gè)方法相似,只不過處理的是實(shí)例方法捉貌。
  • 第三個(gè)方法是將你調(diào)用的不存在的方法重定向到一個(gè)其他聲明了這個(gè)方法的類趴生,只需要你返回一個(gè)有這個(gè)方法的target阀趴。
  • 第四個(gè)方法是將你調(diào)用的不存在的方法打包成NSInvocation傳給你。做完你自己的處理后苍匆,調(diào)用invokeWithTarget:方法讓某個(gè)target觸發(fā)這個(gè)方法刘急。

動(dòng)態(tài)添加方法

重寫了攔截調(diào)用的方法并且返回了YES,我們要怎么處理呢浸踩?有一個(gè)辦法是根據(jù)傳進(jìn)來的SEL
類型的selector動(dòng)態(tài)添加一個(gè)方法叔汁。

首先從外部隱式調(diào)用一個(gè)不存在的方法:

//隱式調(diào)用方法
[target performSelector:@selector(resolveAdd:) withObject:@"test"];

然后,在target對(duì)象內(nèi)部重寫攔截調(diào)用的方法检碗,動(dòng)態(tài)添加方法据块。

void runAddMethod(id self, SEL _cmd, NSString *string){ 
    NSLog(@"add C IMP ", string);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{ 
    //給本類動(dòng)態(tài)添加一個(gè)方法
    if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) { 
        class_addMethod(self, sel, (IMP)runAddMethod, "v@:*"); 
    } 
    return YES;
}

其中class_addMethod的四個(gè)參數(shù)分別是:

  1. Class cls給哪個(gè)類添加方法,本例中是self
  2. SEL name添加的方法折剃,本例中是重寫的攔截調(diào)用傳進(jìn)來的selector另假。
  3. IMP imp方法的實(shí)現(xiàn),C方法的方法實(shí)現(xiàn)可以直接獲得怕犁。如果是OC方法边篮,可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;獲得方法的實(shí)現(xiàn)。
  4. "v@:*"方法的簽名奏甫,代表有一個(gè)參數(shù)的方法戈轿。

關(guān)聯(lián)對(duì)象

現(xiàn)在你準(zhǔn)備用一個(gè)系統(tǒng)的類,但是系統(tǒng)的類并不能滿足你的需求阵子,你需要額外添加一個(gè)屬性思杯。
這種情況的一般解決辦法就是繼承。
但是挠进,只增加一個(gè)屬性色乾,就去繼承一個(gè)類,總是覺得太麻煩類领突。
這個(gè)時(shí)候杈湾,runtime的關(guān)聯(lián)屬性就發(fā)揮它的作用了。
你還可以把添加和獲取關(guān)聯(lián)對(duì)象的方法寫在你需要用到這個(gè)功能的類的類別中攘须,方便使用漆撞。

方法交換

方法交換,顧名思義于宙,就是將兩個(gè)方法的實(shí)現(xiàn)交換浮驳。例如,將A方法和B方法交換捞魁,調(diào)用A方法的時(shí)候至会,就會(huì)執(zhí)行B方法中的代碼,反之亦然谱俭。
方法交換應(yīng)該被保證奉件,在程序中只會(huì)執(zhí)行一次

應(yīng)用實(shí)例

1宵蛀、Json到Model的轉(zhuǎn)化
在開發(fā)中相信最常用的就是接口數(shù)據(jù)需要轉(zhuǎn)化成Model了(當(dāng)然如果你是直接從Dict取值的話。县貌。术陶。),很多開發(fā)者也都使用著名的第三方庫(kù)如JsonModel煤痕、Mantle或MJExtension等梧宫,下面我們使用runtime去解析json來給Model賦值。

原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性摆碉,如果屬性在json中有對(duì)應(yīng)的值塘匣,則將其賦值。
核心方法:在NSObject的分類中添加方法:

- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [self init]) { 
    //(1)獲取類的屬性及屬性對(duì)應(yīng)的類型 
    NSMutableArray * keys = [NSMutableArray array]; 
    NSMutableArray * attributes = [NSMutableArray array]; 
    /* 
      * 例子 
      * name = value3 attribute = T@"NSString",C,N,V_value3 
      * name = value4 attribute = T^i,N,V_value4 
      */
    unsigned int outCount; 
    objc_property_t * properties = class_copyPropertyList([self class], &outCount); 
    for (int i = 0; i < outCount; i ++) { 
        objc_property_t property = properties[i]; 
        //通過property_getName函數(shù)獲得屬性的名字 
        NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding]; 
        [keys addObject:propertyName]; 
        //通過property_getAttributes函數(shù)可以獲得屬性的名字和@encode編碼 
        NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding]; 
        [attributes addObject:propertyAttribute]; } 
        //立即釋放properties指向的內(nèi)存 
        free(properties); 

        //(2)根據(jù)類型給屬性賦值 
        for (NSString * key in keys) { 
            if ([dict valueForKey:key] == nil) continue; 
            [self setValue:[dict valueForKey:key] forKey:key]; 
        } 
    } 
    return self;
}

2巷帝、快速歸檔
有時(shí)候我們要對(duì)一些信息進(jìn)行歸檔忌卤,如用戶信息類UserInfo,這將需要重寫initWithCoder和encodeWithCoder方法楞泼,并對(duì)每個(gè)屬性進(jìn)行encode和decode操作驰徊。那么問題來了:當(dāng)屬性只有幾個(gè)的時(shí)候可以輕松寫完,如果有幾十個(gè)屬性呢现拒?

原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性,并對(duì)屬性進(jìn)行encode和decode操作望侈。
核心方法:在Model的基類中重寫方法:

- (id)initWithCoder:(NSCoder *)aDecoder { 
    if (self = [super init]) { 
        unsigned int outCount; 
        Ivar * ivars = class_copyIvarList([self class], &outCount); 
        for (int i = 0; i < outCount; i ++) { 
            Ivar ivar = ivars[i]; 
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)]; 
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key]; 
        } 
    } 
    return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder { 
    unsigned int outCount; 
    Ivar * ivars = class_copyIvarList([self class], &outCount); 
    for (int i = 0; i < outCount; i ++) { 
        Ivar ivar = ivars[i]; 
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)]; 
        [aCoder encodeObject:[self valueForKey:key] forKey:key]; 
    }
}

3印蔬、訪問私有變量
我們知道,OC中沒有真正意義上的私有變量和方法脱衙,要讓成員變量私有侥猬,要放在m文件中聲明,不對(duì)外暴露捐韩。如果我們知道這個(gè)成員變量的名稱退唠,可以通過runtime獲取成員變量,再通過getIvar來獲取它的值荤胁。
方法:

Ivar ivar = class_getInstanceVariable([Model class], "_str1"); 
NSString * str1 = object_getIvar(model, ivar);

整理來源鏈接:http://www.reibang.com/p/d361f169423b

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瞧预,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子仅政,更是在濱河造成了極大的恐慌垢油,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圆丹,死亡現(xiàn)場(chǎng)離奇詭異滩愁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)辫封,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門硝枉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來廉丽,“玉大人,你說我怎么就攤上這事妻味≌梗” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵弧可,是天一觀的道長(zhǎng)蔑匣。 經(jīng)常有香客問我,道長(zhǎng)棕诵,這世上最難降的妖魔是什么裁良? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮校套,結(jié)果婚禮上价脾,老公的妹妹穿的比我還像新娘。我一直安慰自己笛匙,他們只是感情好侨把,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著妹孙,像睡著了一般秋柄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蠢正,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天骇笔,我揣著相機(jī)與錄音,去河邊找鬼嚣崭。 笑死笨触,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的雹舀。 我是一名探鬼主播芦劣,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼说榆!你這毒婦竟也來了虚吟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤签财,失蹤者是張志新(化名)和其女友劉穎稍味,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荠卷,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡模庐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了油宜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掂碱。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡怜姿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疼燥,到底是詐尸還是另有隱情沧卢,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布醉者,位于F島的核電站但狭,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏撬即。R本人自食惡果不足惜立磁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望剥槐。 院中可真熱鬧唱歧,春花似錦、人聲如沸粒竖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蕊苗。三九已至沿后,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間朽砰,已是汗流浹背尖滚。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锅移,地道東北人熔掺。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓饱搏,卻偏偏與公主長(zhǎng)得像非剃,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子推沸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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