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

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


在我們知道了 Objective-C 中類的本質(zhì),以及它的消息分發(fā)機制后,我們就可以來看看那些與 runtime 相關(guān)的的函數(shù)了。當(dāng)然,我們只會講比較常見的那些。

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


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

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

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

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

  • 第一個參數(shù) id object 顯然就是你要設(shè)置關(guān)聯(lián)對象的那個對象。
  • 第二個參數(shù) const void *key 就是用來區(qū)分不同的關(guān)聯(lián)對象的 key,因為想讓兩個 key 匹配到同一個關(guān)聯(lián)對象就必須是完全相等的指針,所以我們一般用靜態(tài)全局變量來作為 key。
static const void *AssociatedKey = "AssociatedKey";
  • 第三個參數(shù) id value 就是要關(guān)聯(lián)的對象了。
  • 第四個參數(shù) objc_AssociationPolicy policy 指的是關(guān)聯(lián)對象的存儲策略,它是一個枚舉,可以與 property 的 attribute 相對應(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 中,我們無法添加 property,因為無法添加實例變量。那么,我們現(xiàn)在就可以通過關(guān)聯(lián)對象來實現(xiàn)在 category 中添加屬性的功能了。

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

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

如果這個時候我們直接在外部訪問這個屬性, 那個程序是會 crash 的,不信你可以試試??,編譯器會說:

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

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

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)在我們再進行讀寫操作,程序就不會 crash 了。當(dāng)然,沒有必要的情況下,還是不要濫用關(guān)聯(lián)對象, 否則有可能會出現(xiàn)一些難以發(fā)現(xiàn)的bug。

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


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

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

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

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

可以直接替換方法,當(dāng)需要的方法不存在時,會先調(diào)用 class_addMethod 來添加一個新的方法。會返回替換前的實現(xiàn)函數(shù)指針 。

Method class_getInstanceMethod(Class cls, SEL name);

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

IMP method_setImplementation(Method m, IMP imp);

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

void method_exchangeImplementations(Method m1, Method m2)

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

我們用 method_exchangeImplementations 來簡單的嘗試一下 method swizzling,我添加了一個 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

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

我們在看一下調(diào)用的結(jié)果

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

當(dāng)然 method swizzling 是一把雙刃劍,我們可以用它來進行黑盒測試,在真正的項目中如果用 method swizzling 一定要格外小心。

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


當(dāng)我們的對象接收到一個無法解讀的消息時,就會進入消息轉(zhuǎn)發(fā)。消息轉(zhuǎn)發(fā)分為兩大階段,第一階段是動態(tài)方法解析,第二階段是完整的消息轉(zhuǎn)發(fā)。

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


要實現(xiàn)動態(tài)方法解析只要重寫兩個方法:

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

這兩個方法傳進來的參數(shù) selector 就是那個無法解析的方法,我們可以根據(jù)這個 selector 來動態(tài)的為這個類添加方法。比如像下面這樣:

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

@implementation CYClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(someSelector)) {       // 對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);

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

當(dāng) resolveInstanceMethod: 返回 NO 時,就會進入消息轉(zhuǎn)發(fā)的第二階段 完整的消息轉(zhuǎn)發(fā)機制。

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


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

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

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

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

AnotherClass 的實例就是我們用來作為備援接受者的對象,我們在 AnotherClass 中實現(xiàn)了 unrecognizedSel 方法:

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

然后我們再給 CYClass 的實例發(fā)送 unrecognizedSel 的消息就不會 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

如果這一步我們也沒有提供一個備援的接收者,那么就會進入最后一步 - forwardInvocation: 方法,系統(tǒng)會把所有與那條消息相關(guān)的信息全部封裝在一個 NSInvocation 對象中,我們可以在直接改變調(diào)用的目標(biāo), 也可以修改消息的內(nèi)容后再進行轉(zhuǎn)發(fā)。我們把前一個方法去掉,然后重寫一下 - 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

需要注意的是我們還要重寫- methodSignatureForSelector: 方法,因為生成 NSInvocation 對象會調(diào)用到這個方法,否則會拋出異常。關(guān)于 forwardInvocation 了解的還不是很多,所以例子比較簡單,以后有了更深的理解后會再加上。

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

總結(jié)


到這里對于 runtime 的簡單理解與使用就基本結(jié)束了。總的來說,理解了 Objective-C 的運行時會讓我們的代碼更加靈活,當(dāng)然也會增大維護的難度。不過想要學(xué)好 Objective-C 這門語言,runtime 是必不可少的!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,967評論 2 374

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