Objective-C runtime 的簡(jiǎn)單理解與使用(二)

簡(jiǎn)書(shū)上的所有內(nèi)容都可以在我的個(gè)人博客上找到(打個(gè)廣告??)


在我們知道了 Objective-C 中類的本質(zhì)续扔,以及它的消息分發(fā)機(jī)制后,我們就可以來(lái)看看那些與 runtime 相關(guān)的的函數(shù)了案疲。當(dāng)然芥喇,我們只會(huì)講比較常見(jiàn)的那些权逗。

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


關(guān)聯(lián)對(duì)象,顧名思義捣染,就是給某對(duì)象關(guān)聯(lián)許多其他的對(duì)象骄瓣。這些對(duì)象通過(guò) key 來(lái)區(qū)分。

與關(guān)聯(lián)對(duì)象相關(guān)的函數(shù)有三個(gè):

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id object, const void *key);
objc_removeAssociatedObjects(id object);

從函數(shù)名我們也可以看出來(lái)耍攘,這三個(gè)函數(shù)分別是用來(lái)設(shè)置榕栏,獲取和移除關(guān)聯(lián)對(duì)象的。這里要解釋一下的是他們的參數(shù)蕾各。

  • 第一個(gè)參數(shù) id object 顯然就是你要設(shè)置關(guān)聯(lián)對(duì)象的那個(gè)對(duì)象扒磁。
  • 第二個(gè)參數(shù) const void *key 就是用來(lái)區(qū)分不同的關(guān)聯(lián)對(duì)象的 key,因?yàn)橄胱寖蓚€(gè) key 匹配到同一個(gè)關(guān)聯(lián)對(duì)象就必須是完全相等的指針式曲,所以我們一般用靜態(tài)全局變量來(lái)作為 key妨托。
static const void *AssociatedKey = "AssociatedKey";
  • 第三個(gè)參數(shù) id value 就是要關(guān)聯(lián)的對(duì)象了。
  • 第四個(gè)參數(shù) objc_AssociationPolicy policy 指的是關(guān)聯(lián)對(duì)象的存儲(chǔ)策略吝羞,它是一個(gè)枚舉兰伤,可以與 property 的 attribute 相對(duì)應(yīng):
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,                         // assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,             // nonatomic, retain
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,                 // nonatomic, copy
    OBJC_ASSOCIATION_RETAIN = 01401,                     // retain
    OBJC_ASSOCIATION_COPY = 01403                       // copy
};

大家知道,在 category 中钧排,我們無(wú)法添加 property敦腔,因?yàn)闊o(wú)法添加實(shí)例變量。那么恨溜,我們現(xiàn)在就可以通過(guò)關(guān)聯(lián)對(duì)象來(lái)實(shí)現(xiàn)在 category 中添加屬性的功能了符衔。

我們現(xiàn)在 CYClass 類的拓展中聲明了一個(gè)屬性

@interface CYClass (Property)
@property (nonatomic, copy)NSString *aString;
@end

如果這個(gè)時(shí)候我們直接在外部訪問(wèn)這個(gè)屬性, 那個(gè)程序是會(huì) crash 的糟袁,不信你可以試試??判族,編譯器會(huì)說(shuō):

'-[CYClass setAString:]: unrecognized selector sent to instance 0x1001060a0'

所以我們給它加上 setter 和 getter 方法, 并且在這兩個(gè)方法中給它設(shè)置關(guān)聯(lián)對(duì)象:

static void *aStringKey = "aStringKey";

@implementation CYClass (Property)

- (void)setAString:(NSString *)newString{
    objc_setAssociatedObject(self, aStringKey, newString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)aString{
    return objc_getAssociatedObject(self, aStringKey);
}
@end

現(xiàn)在我們?cè)龠M(jìn)行讀寫(xiě)操作项戴,程序就不會(huì) crash 了形帮。當(dāng)然,沒(méi)有必要的情況下肯尺,還是不要濫用關(guān)聯(lián)對(duì)象, 否則有可能會(huì)出現(xiàn)一些難以發(fā)現(xiàn)的bug躯枢。

方法調(diào)配(Method Swizzling)


在前一篇博客中我們知道了每個(gè)類中的方法是以 objc_method 結(jié)構(gòu)體的形式放在 methodLists 中的则吟。每一個(gè) selector 對(duì)應(yīng)了一個(gè)實(shí)現(xiàn)的函數(shù)的指針 IMP。而 method swizzling 技術(shù)就是通過(guò)交換這個(gè)函數(shù)指針來(lái)實(shí)現(xiàn)的锄蹂。

我們最好在 +load 方法中使用 method swizzling氓仲,因?yàn)?+load 方法對(duì)于加入運(yùn)行期中的每個(gè)類及分類都會(huì)調(diào)用且只調(diào)用一次。所以在這里交換方法是最安全的。

我們來(lái)看一下蘋果為我們提供了哪些API來(lái)實(shí)現(xiàn) method swizzling:

IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);

可以直接替換方法敬扛,當(dāng)需要的方法不存在時(shí)晰洒,會(huì)先調(diào)用 class_addMethod 來(lái)添加一個(gè)新的方法。會(huì)返回替換前的實(shí)現(xiàn)函數(shù)指針 啥箭。

Method class_getInstanceMethod(Class cls, SEL name);

根據(jù)類和 selector 得到 method谍珊,用來(lái)作為下面兩個(gè)方法的參數(shù)。

IMP method_setImplementation(Method m, IMP imp);

直接為一個(gè)方法設(shè)置它的實(shí)現(xiàn)急侥,返回之前的實(shí)現(xiàn)函數(shù)指針

void method_exchangeImplementations(Method m1, Method m2)

交換兩個(gè)方法的實(shí)現(xiàn)砌滞,實(shí)際上就是調(diào)用了兩次 method_setImplementation,并且是線程安全的坏怪。

我們用 method_exchangeImplementations 來(lái)簡(jiǎn)單的嘗試一下 method swizzling贝润,我添加了一個(gè) NSString 的分類,用我自己的方法交換了系統(tǒng)的 lowercaseString 方法:

@implementation NSString (Swizzling)
+ (void)load {
    Method originalMethod = class_getInstanceMethod([self class], @selector(lowercaseString));
    Method swappedMthod = class_getInstanceMethod([self class], @selector(swizzle_lowercaseString));
    method_exchangeImplementations(originalMethod, swappedMthod);
}

- (NSString *)swizzle_lowercaseString {
    NSString *lowercase = [self swizzle_lowercaseString];
    NSLog(@"FROM: %@  TO:  %@", self, lowercase);
    return lowercase;
}
@end

可能有人會(huì)覺(jué)得在自己新寫(xiě)的 swizzle_lowercaseString 方法中又調(diào)用 [self swizzle_lowercaseString] 會(huì)導(dǎo)致死循環(huán)铝宵,其實(shí)在交換了方法以后我們調(diào)用原來(lái)的 lowercaseString 方法就會(huì)進(jìn)入這個(gè)方法的實(shí)現(xiàn)打掘,而這時(shí)候調(diào)用 swizzle_lowercaseString 其實(shí)調(diào)用的是系統(tǒng)原來(lái)的方法,所以是不會(huì)產(chǎn)生死循環(huán)的鹏秋。這里理解起來(lái)可能有點(diǎn)奇怪尊蚁。

我們?cè)诳匆幌抡{(diào)用的結(jié)果

2016-03-11 20:01:05.645 Example[4129:101067] FROM: Hello World  TO:  hello world

當(dāng)然 method swizzling 是一把雙刃劍,我們可以用它來(lái)進(jìn)行黑盒測(cè)試拼岳,在真正的項(xiàng)目中如果用 method swizzling 一定要格外小心枝誊。

消息轉(zhuǎn)發(fā)機(jī)制(Message Forwarding)


當(dāng)我們的對(duì)象接收到一個(gè)無(wú)法解讀的消息時(shí),就會(huì)進(jìn)入消息轉(zhuǎn)發(fā)惜纸。消息轉(zhuǎn)發(fā)分為兩大階段叶撒,第一階段是動(dòng)態(tài)方法解析,第二階段是完整的消息轉(zhuǎn)發(fā)耐版。

動(dòng)態(tài)方法解析(dynamic method resolution)


要實(shí)現(xiàn)動(dòng)態(tài)方法解析只要重寫(xiě)兩個(gè)方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel; // 處理無(wú)法識(shí)別的實(shí)例方法
+ (BOOL)resolveClassMethod:(SEL)sel;    // 處理無(wú)法識(shí)別的類方法

這兩個(gè)方法傳進(jìn)來(lái)的參數(shù) selector 就是那個(gè)無(wú)法解析的方法祠够,我們可以根據(jù)這個(gè) selector 來(lái)動(dòng)態(tài)的為這個(gè)類添加方法。比如像下面這樣:

void dynamicMethod(id self, SEL _cmd) {
    // do something here
}

@implementation CYClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(someSelector)) {       // 對(duì)selector做一些邏輯判斷
        class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:");  // 為類添加方法
        return YES;
    } else {
        return NO;
    }
}
@end

還要提一下 class_addMethod 函數(shù):

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types);

它的最后一個(gè)參數(shù)是用來(lái)描述這個(gè)函數(shù)的返回值和參數(shù)類型的粪牲,稱之為 類型編碼(Type Encoding)古瓤。在前面那個(gè)例子里的 "v@:" 中, v 表示返回值為 void腺阳, @ 表示第一個(gè)參數(shù)是 id落君, : 表示第二個(gè)參數(shù)類型是 SEL 。更多的類型編碼可以看這里

當(dāng) resolveInstanceMethod: 返回 NO 時(shí)亭引,就會(huì)進(jìn)入消息轉(zhuǎn)發(fā)的第二階段 完整的消息轉(zhuǎn)發(fā)機(jī)制绎速。

完整的消息轉(zhuǎn)發(fā)機(jī)制


完整的消息轉(zhuǎn)發(fā)主要涉及兩個(gè)方法:

- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

如果在 + resolveInstanceMethod: 方法中返回了 NO 那么就會(huì)執(zhí)行 - forwardingTargetForSelector: 方法。在這個(gè)方法內(nèi)我們可以給對(duì)象返回一個(gè)備援的接受者來(lái)處理這個(gè)位置的信息焙蚓。在 CYClass 的實(shí)現(xiàn)中我們這么寫(xiě):

@implementation CYClass
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(unrecognizedSel)) {
        return [AnotherClass new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

AnotherClass 的實(shí)例就是我們用來(lái)作為備援接受者的對(duì)象纹冤,我們?cè)?AnotherClass 中實(shí)現(xiàn)了 unrecognizedSel 方法:

@implementation AnotherClass
- (void)unrecognizedSel {
    NSLog(@"forwarding target for unrecognized selector in AnotherClass");
}
@end

然后我們?cè)俳o CYClass 的實(shí)例發(fā)送 unrecognizedSel 的消息就不會(huì) crash 了:

CYClass *c = [CYClass new];
[c performSelector:@selector(unrecognizedSel)];

// 打印結(jié)果
2016-03-12 12:46:30.608 example[1577:19943] forwarding target for unrecognized selector in AnotherClass

如果這一步我們也沒(méi)有提供一個(gè)備援的接收者洒宝,那么就會(huì)進(jìn)入最后一步 - forwardInvocation: 方法,系統(tǒng)會(huì)把所有與那條消息相關(guān)的信息全部封裝在一個(gè) NSInvocation 對(duì)象中萌京,我們可以在直接改變調(diào)用的目標(biāo)雁歌, 也可以修改消息的內(nèi)容后再進(jìn)行轉(zhuǎn)發(fā)。我們把前一個(gè)方法去掉知残,然后重寫(xiě)一下 - forwardInvocation: 方法:

@implementation CYClass
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    if (sel == @selector(unrecognizedSel)) {
        [anInvocation invokeWithTarget:[AnotherClass new]];
    } else {
        [super forwardInvocation: anInvocation];
    }
}
@end

需要注意的是我們還要重寫(xiě)- methodSignatureForSelector: 方法靠瞎,因?yàn)樯?NSInvocation 對(duì)象會(huì)調(diào)用到這個(gè)方法,否則會(huì)拋出異常橡庞。關(guān)于 forwardInvocation 了解的還不是很多较坛,所以例子比較簡(jiǎn)單,以后有了更深的理解后會(huì)再加上扒最。

消息轉(zhuǎn)發(fā)的全過(guò)程

總結(jié)


到這里對(duì)于 runtime 的簡(jiǎn)單理解與使用就基本結(jié)束了丑勤。總的來(lái)說(shuō)吧趣,理解了 Objective-C 的運(yùn)行時(shí)會(huì)讓我們的代碼更加靈活法竞,當(dāng)然也會(huì)增大維護(hù)的難度。不過(guò)想要學(xué)好 Objective-C 這門語(yǔ)言强挫,runtime 是必不可少的岔霸!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市俯渤,隨后出現(xiàn)的幾起案子呆细,更是在濱河造成了極大的恐慌,老刑警劉巖八匠,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件絮爷,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡梨树,警方通過(guò)查閱死者的電腦和手機(jī)坑夯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)抡四,“玉大人柜蜈,你說(shuō)我怎么就攤上這事≈秆玻” “怎么了淑履?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)藻雪。 經(jīng)常有香客問(wèn)我秘噪,道長(zhǎng),這世上最難降的妖魔是什么阔涉? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任缆娃,我火速辦了婚禮,結(jié)果婚禮上瑰排,老公的妹妹穿的比我還像新娘贯要。我一直安慰自己,他們只是感情好椭住,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布崇渗。 她就那樣靜靜地躺著,像睡著了一般京郑。 火紅的嫁衣襯著肌膚如雪宅广。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天些举,我揣著相機(jī)與錄音跟狱,去河邊找鬼。 笑死户魏,一個(gè)胖子當(dāng)著我的面吹牛驶臊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播叼丑,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼关翎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了鸠信?” 一聲冷哼從身側(cè)響起纵寝,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎星立,沒(méi)想到半個(gè)月后爽茴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贞铣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年闹啦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辕坝。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窍奋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出酱畅,到底是詐尸還是另有隱情琳袄,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布纺酸,位于F島的核電站窖逗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏餐蔬。R本人自食惡果不足惜碎紊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一佑附、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧仗考,春花似錦音同、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至锅锨,卻和暖如春叽赊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背必搞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工必指, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人恕洲。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓取劫,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親研侣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子谱邪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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

  • 前言 到了今天終于要"出院"了,要總結(jié)一下住院幾天的收獲庶诡,談?wù)凴untime到底能為我們開(kāi)發(fā)帶來(lái)些什么好處惦银。當(dāng)然它...
    一縷殤流化隱半邊冰霜閱讀 23,361評(píng)論 56 317
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,709評(píng)論 0 9
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 731評(píng)論 0 2
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識(shí)末誓,它使得 Objective-C 如虎添翼扯俱,具備了靈活的...
    lylaut閱讀 800評(píng)論 0 4
  • 我們常常會(huì)聽(tīng)說(shuō) Objective-C 是一門動(dòng)態(tài)語(yǔ)言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢喇澡?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,192評(píng)論 0 7