Objective-C 對(duì)象模型及應(yīng)用整理

本文參考自 http://blog.devtang.com/2013/10/15/objective-c-object-model/ 以及 http://ios.jobbole.com/81657/ 模狭。 純粹是對(duì)文章內(nèi)容的整理和整合,供自己以后查閱评矩,版權(quán)歸原作者所有结洼。

isa 指針

什么數(shù)據(jù)結(jié)構(gòu)才能稱之為對(duì)象勿她?

每個(gè)對(duì)象都有類。這是面向?qū)ο蟮幕靖拍睿窃贠bjective-C中谤饭,它對(duì)數(shù)據(jù)結(jié)構(gòu)也一樣年枕。含有一個(gè)指針且該指針可以正確指向類的數(shù)據(jù)結(jié)構(gòu)炫欺,都可以被視作為對(duì)象。

在Objective-C中熏兄,對(duì)象的類是isa指針決定的品洛。isa指針指向?qū)ο笏鶎俚念悺?/p>

類結(jié)構(gòu)圖

實(shí)際上,Objective-C中對(duì)象最基本的定義是這樣的:


objc_object
Class

這說(shuō)的是:任何帶有以指針開始并指向類結(jié)構(gòu)的結(jié)構(gòu)都可以被視作objc_object摩桶。
我們還可以看到桥状,Class 也是一個(gè)包含 isa 指針的結(jié)構(gòu)體。(圖中除了 isa 外還有其它成員變量硝清,但那是為了兼容非 2.0 版的 Objective-C 的遺留邏輯辅斟,大家可以忽略它。)
Objective-C中對(duì)象最重要的特點(diǎn)是你可以發(fā)送消息給它們:

[@"stringValue"  writeToFile:@"/file.txt" atomically:YES encoding:NSUTF8StringEncoding error:NULL];

這能工作是因?yàn)镺bjective-C對(duì)象(這兒是NSCFString)在發(fā)送消息時(shí)芦拿,運(yùn)行時(shí)庫(kù)會(huì)追尋著對(duì)象的isa指針得到了對(duì)象所屬的類(這兒是NSCFString類)士飒。這個(gè)類包含了能應(yīng)用于這個(gè)類的所有實(shí)例方法和指向超類的指針以便可以找到父類的實(shí)例方法查邢。運(yùn)行時(shí)庫(kù)檢查這個(gè)類和其超類的方法列表,找到一個(gè)匹配這條消息的方法(在上面的代碼里变汪,是NSString類的writeToFile:atomically:encoding:error方法)侠坎。運(yùn)行時(shí)庫(kù)基于那個(gè)方法調(diào)用函數(shù)(IMP)。重點(diǎn)就是類要定義這個(gè)你發(fā)送給對(duì)象的消息裙盾。

什么是元類(meta class)实胸?

你可以發(fā)送消息給一個(gè)類:

NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding];

在這個(gè)示例里,defaultStringEncoding被發(fā)送給了 NSString類番官。

因此Objective-C中每個(gè)類本身(Class)也是一個(gè)對(duì)象庐完。如上面圖Class所展示的,這意味著類結(jié)構(gòu)必須以一個(gè)isa指針開始徘熔,從而可以和objc_object在二進(jìn)制層面兼容门躯。為了調(diào)用類里的方法,類的isa指針必須指向包含這些類方法的類結(jié)構(gòu)體酷师。這個(gè)類結(jié)構(gòu)體就是元類 (metaclass)讶凉。
簡(jiǎn)單說(shuō)就是:

  • 當(dāng)你給對(duì)象發(fā)送消息時(shí),消息是在尋找這個(gè)對(duì)象的類的方法列表山孔。
  • 當(dāng)你給類發(fā)消息時(shí)懂讯,消息是在尋找這個(gè)類的元類的方法列表。

元類是必不可少的台颠,因?yàn)樗鎯?chǔ)了類的類方法褐望。每個(gè)類都必須有獨(dú)一無(wú)二的元類,因?yàn)槊總€(gè)類都有獨(dú)一無(wú)二的類方法串前。每個(gè)對(duì)象的isa所指的是一個(gè)元類的實(shí)例瘫里。那么這個(gè)實(shí)例所屬的類是如何定義的呢?這就引出了:

元類的類是什么荡碾?

元類谨读,就像之前的類一樣,它也是一個(gè)對(duì)象坛吁。你也可以調(diào)用它的方法漆腌。自然的,這就意味著他必須也有一個(gè)類阶冈。

如類結(jié)構(gòu)圖所示闷尿,所有的元類都使用根元類(繼承體系中處于頂端的類的元類)作為他們的類。這就意味著所有NSObject的子類(大多數(shù)類)的元類都會(huì)以NSObject的元類作為他們的類女坑。

根據(jù)這個(gè)規(guī)則填具,所有的元類使用根元類作為他們的類,根元類的元類則就是它自己。也就是說(shuō)基類的元類的isa指針指向他自己劳景。

驗(yàn)證

下面的代碼在運(yùn)行時(shí)創(chuàng)建了一個(gè)NSError的子類誉简,并且添加了一個(gè)方法:

Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
objc_registerClassPair(newClass);

ReportFunction函數(shù)就是添加的實(shí)例方法,具體實(shí)現(xiàn)如下

void ReportFunction(id self, SEL _cmd)
{
    NSLog(@"This object is %p.", self);
    NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);
 
    Class currentClass = [self class];
    for (int i = 1; i < 5; i++)
    {
        NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
        currentClass = object_getClass(currentClass);
    }
 
    NSLog(@"NSObject's class is %p", [NSObject class]);
    NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
}

表面上看來(lái)盟广,這相當(dāng)簡(jiǎn)單闷串。在運(yùn)行時(shí)創(chuàng)建一個(gè)類只需要3個(gè)步驟:

  1. 為”class pair”分配內(nèi)存 (使用objc_allocateClassPair).
  2. 添加方法或成員變量到有需要的類里 (我已經(jīng)使用class_addMethod添加了一個(gè)方法).
  3. 注冊(cè)類以便它能使用 (使用objc_registerClassPair).

這里解釋一下 SEL和IMP

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

其中,Apple源碼里并沒有給出objc_selector的定義筋量,這里用例子來(lái)說(shuō)明:

@interface NSObject (Sark)
+ (void)foo;
@end

@implementation NSObject (Sark)

- (void)foo
{
    NSLog(@"IMP: -[NSObject(Sark) foo]");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SEL sel = @selector(foo);
        NSLog(@"%s", (char *)sel);
        NSLog(@"%p", sel);

        const char *selName = [@"foo" UTF8String];
        SEL sel2 = sel_registerName(selName);
        NSLog(@"%s", (char *)sel2);
        NSLog(@"%p", sel2);
    }
    return 0;
}

輸出結(jié)果:

2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114

因此可以發(fā)現(xiàn)烹吵,Objective-C在編譯時(shí),會(huì)根據(jù)方法的名字生成一個(gè)用來(lái)區(qū)分這個(gè)方法的唯一的一個(gè)ID桨武。只要方法名稱相同肋拔,那么它們的ID就是相同的。
兩個(gè)類之間呀酸,不管它們是父類與子類的關(guān)系凉蜂,還是之間沒有這種關(guān)系,只要方法名相同性誉,那么它的SEL就是一樣的窿吩。每一個(gè)方法都對(duì)應(yīng)著一個(gè)SEL。編譯器會(huì)根據(jù)每個(gè)方法的方法名為那個(gè)方法生成唯一的SEL错览。這些SEL組成了一個(gè)Set集合爆存,當(dāng)我們?cè)谶@個(gè)集合中查找某個(gè)方法時(shí),只需要去找這個(gè)方法對(duì)應(yīng)的SEL即可蝗砾。而SEL本質(zhì)是一個(gè)字符串,所以直接比較它們的地址即可携冤。

那么什么是IMP呢悼粮?
看其定義, IMP本質(zhì)就是一個(gè)函數(shù)指針曾棕,這個(gè)被指向的函數(shù)包含一個(gè)接收消息的對(duì)象id扣猫,調(diào)用方法的SEL,以及一些方法參數(shù)翘地,并返回一個(gè)id申尤。因此我們可以通過SEL獲得它所對(duì)應(yīng)的IMP,在取得了函數(shù)指針之后衙耕,也就意味著我們?nèi)〉昧诵枰獔?zhí)行方法的代碼入口昧穿,這樣我們就可以像普通的C語(yǔ)言函數(shù)調(diào)用一樣使用這個(gè)函數(shù)指針。

那么什么是方法列表呢橙喘?
方法列表就是在圖objc_object里时鸵,objc_class結(jié)構(gòu)中的成員 struct objc_method_list **methodLists.

重點(diǎn)參考自:http://chun.tips/blog/2014/11/06/bao-gen-wen-di-objective[nil]c-runtime(3)[nil]-xiao-xi-he-category/

運(yùn)行ReportFunction,我們需要?jiǎng)?chuàng)建一個(gè)動(dòng)態(tài)實(shí)例來(lái)創(chuàng)建類調(diào)用report方法:

id instanceOfNewClass = [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil];
[instanceOfNewClass performSelector:@selector(report)];
[instanceOfNewClass release];

這里沒有聲明report方法,但我使用performSelector:調(diào)用它饰潜,所以編譯器不會(huì)給出警告初坠。函數(shù)使用object_getClass跟蹤isa指針,因?yàn)閕sa指針是類的保護(hù)成員(你不能直接接收其他對(duì)象的isa指針)彭雾。ReportFunction不使用類方法碟刺,因?yàn)樵陬悓?duì)象里調(diào)用類方法不能返回元類,它會(huì)再次返回這個(gè)類(因此[NSString class]會(huì)返回NSString類而不是NSString元類).
ReportFunction函數(shù)會(huì)沿著isa進(jìn)行檢索薯酝,來(lái)告訴我們class半沽,meta-class以及meta-class的class是什么樣的情況:

This object is 0x10010c810.
Class is RuntimeErrorSubclass, and super is NSError.
Following the isa pointer 1 times gives 0x10010c600
Following the isa pointer 2 times gives 0x10010c630
Following the isa pointer 3 times gives 0x7fff71038480
Following the isa pointer 4 times gives 0x7fff71038480
NSObject's class is 0x7fff710384a8
NSObject's meta class is 0x7fff71038480

觀察isa到達(dá)過的地址的值:

  • 對(duì)象的地址是 0x10010c810
  • 類的地址是 0x10010c600
  • 元類的地址是 0x10010c630
  • 根元類(NSObject的元類)的地址是 0x7fff71038480
  • NSObject元類的類是它本身.

這些地址的值并不重要,重要的是它們說(shuō)明了文中討論的從類到meta-class到NSObject的meta-class的整個(gè)流程蜜托。

系統(tǒng)相關(guān)API及應(yīng)用

ias swizzling的應(yīng)用

系統(tǒng)提供的 KVO 的實(shí)現(xiàn)抄囚,就利用了動(dòng)態(tài)地修改 isa 指針的值的技術(shù)。

Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the [class] method to determine the class of an object instance.

Method Swizzling API 說(shuō)明

Objective-C 提供了以下 API 來(lái)動(dòng)態(tài)替換類方法或?qū)嵗椒ǖ膶?shí)現(xiàn):
class_replaceMethod 替換類方法的定義
method_exchangeImplementations 交換 2 個(gè)方法的實(shí)現(xiàn)
method_setImplementation 設(shè)置 1 個(gè)方法的實(shí)現(xiàn)

這 3 個(gè)方法有一些細(xì)微的差別橄务,給大家介紹如下:

  • class_replaceMethod
    在蘋果的文檔(如下圖所示)中能看到幔托,它有兩種不同的行為。當(dāng)類中沒有想替換的原方法時(shí)蜂挪,該方法會(huì)調(diào)用class_addMethod
    來(lái)為該類增加一個(gè)新方法重挑,也因?yàn)槿绱耍琧lass_replaceMethod在調(diào)用時(shí)需要傳入types參數(shù)棠涮,而method_exchangeImplementations和method_setImplementation卻不需要谬哀。
  • method_exchangeImplementations 的內(nèi)部實(shí)現(xiàn)相當(dāng)于調(diào)用了 2 次method_setImplementation方法,從蘋果的文檔中能清晰地了解到(如下圖所示)

從以上的區(qū)別我們可以總結(jié)出這 3 個(gè) API 的使用場(chǎng)景:

  • class_replaceMethod, 當(dāng)需要替換的方法可能有不存在的情況時(shí)严肪,可以考慮使用該方法史煎。
  • method_exchangeImplementations,當(dāng)需要交換 2 個(gè)方法的實(shí)現(xiàn)時(shí)使用驳糯。(常用于使用自定義的類的方法來(lái)hack掉iOS SDK提供的方法)
  • method_setImplementation 最簡(jiǎn)單的用法篇梭,當(dāng)僅僅需要為一個(gè)方法設(shè)置其實(shí)現(xiàn)方式時(shí)使用。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末酝枢,一起剝皮案震驚了整個(gè)濱河市恬偷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌帘睦,老刑警劉巖袍患,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異竣付,居然都是意外死亡诡延,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門古胆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)孕暇,“玉大人,你說(shuō)我怎么就攤上這事⊙希” “怎么了隧哮?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)座舍。 經(jīng)常有香客問我沮翔,道長(zhǎng),這世上最難降的妖魔是什么曲秉? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任采蚀,我火速辦了婚禮,結(jié)果婚禮上承二,老公的妹妹穿的比我還像新娘榆鼠。我一直安慰自己,他們只是感情好亥鸠,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布妆够。 她就那樣靜靜地躺著,像睡著了一般负蚊。 火紅的嫁衣襯著肌膚如雪神妹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天家妆,我揣著相機(jī)與錄音鸵荠,去河邊找鬼。 笑死伤极,一個(gè)胖子當(dāng)著我的面吹牛蛹找,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哨坪,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼庸疾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了齿税?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤炊豪,失蹤者是張志新(化名)和其女友劉穎凌箕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體词渤,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牵舱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缺虐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芜壁。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出慧妄,到底是詐尸還是另有隱情顷牌,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布塞淹,位于F島的核電站窟蓝,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏饱普。R本人自食惡果不足惜运挫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望套耕。 院中可真熱鬧谁帕,春花似錦、人聲如沸冯袍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)颠猴。三九已至关划,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間翘瓮,已是汗流浹背贮折。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留资盅,地道東北人调榄。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像呵扛,于是被迫代替她去往敵國(guó)和親每庆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

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