IOS開發(fā)Objective-C?Runtime使用示例詳解
前言
Runtime 是使用 C 和匯編實現(xiàn)的運行時代碼庫,Objective-C 中有很多語言特性都是通過它來實現(xiàn)。了解 Runtime 開發(fā)可以幫助我們更靈活的使用 Objective-C 這門語言,我們可以將程序功能推遲到運行時再去決定怎么做,還可以利用 Runtime 來解決項目開發(fā)中的一些設(shè)計和技術(shù)問題,使開發(fā)過程更加具有靈活性。
一些關(guān)鍵字
- self:類的隱藏參數(shù)變量,指向當(dāng)前調(diào)用方法的對象
- super:是編譯器的標(biāo)示符,通過 super 調(diào)用方法會被翻譯成 objc_msgSendSuper(self, _cmd,…)
- SEL:以方法名為內(nèi)容的 C 字符串
- IMP:指向方法實現(xiàn)的函數(shù)指針
- id:指向類對象或?qū)嵗龑ο蟮闹羔?/li>
- isa:為 id 對象所屬類型 (objc_class),Objc 中的繼承就是通過 isa 指針找到 objc_class,然后再通過 super_class 去找對應(yīng)的父類
- metaclass:在 Objc 中,類本身也是對象,實例對象的 isa 指向它所屬的類,而類對象的 isa 指向元類 (metaclass),元類的 isa 直接指向根元類,根元類的isa指向它自己,它們之間的關(guān)系如下圖所示。
消息傳遞 (Messaging)
Objective-C 對于調(diào)用對象的某個方法這種行為叫做給對象發(fā)送消息,實際上就是沿著它的 isa 指針去查找真正的函數(shù)地址。下面我們來了解一下這個過程:
我們寫一個給對象發(fā)送消息的代碼
[array insertObject:obj atIndex:5];
編譯器首先會將上面代碼翻譯成這種樣子
objc_msgSend(array, @selector(insertObject:atIndex:), obj, 5);
系統(tǒng)在運行時會通過 array 對象的 isa 指針找到對應(yīng)的 class(如果是給類發(fā)消息,則找到的是metaclass),然后在 class 的 cache 方法列表中用 SEL 去找對應(yīng) method,如果找不到便去 class 的方法列表中去找,如果在方法列表中也找不對對應(yīng) method 時,便沿著繼承體系繼續(xù)向上查找,找到后將 method 放入 cache,以便下次能快速定位,然后再去執(zhí)行 method 的 IMP,找不到時系統(tǒng)便報錯:unrecognized selector sent to insertObject:atIndex:
Runtime 提供了三種方法避免因為找不到方法而崩潰
當(dāng)找不到方法實現(xiàn)時,Runtime 會先發(fā)送 +resolveInstanceMethod: 或 +resolveClassMethod: 消息,我們可以重寫它然后為對象指定一個處理方法。
void dynamicXXXMethod(id obj, SEL _cmd) { NSLog(@"ok..."); } + (BOOL)resolveInstanceMethod:(SEL)aSEL { if(aSEL == @selector(xxx:)) { class_addMethod([self class], aSEL, (IMP)dynamicXXXMethod, "v@:"); return YES; } return [super resolveInstanceMethod]; }
class_addMethod 方法的最后一個參數(shù)用來指定所添加方法的參數(shù)及返回值,叫 Type Encodings。
如果 resolve 方法返回 NO,Runtime 會發(fā)送 -forwardingTargetForSelector: 消息,允許我們將消息轉(zhuǎn)發(fā)給能處理它的其它對象。
- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(xxx:)){ return otherObject; } return [super forwardingTargetForSelector:aSelector]; }
當(dāng) -forwardingTargetForSelector: 返回 nil 時,Runtime 會發(fā)送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。我們可以選擇忽略消息、拋出異常、將消息轉(zhuǎn)由當(dāng)前對象或其它對象的任意消息來處理。
//根據(jù) SEL 生成 NSInvocation 對象,然后再由 -forwardInvocation: 方法進行轉(zhuǎn)發(fā)。 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature *signature = [super methodSignatureForSelector:aSelector]; if (!signature) { signature = [otherObject instanceMethodSignatureForSelector:aSelector]; } return signature; } - (void)forwardInvocation:(NSInvocation *)invocation { SEL sel = invocation.selector; if([otherObject respondsToSelector:sel]) { [invocation invokeWithTarget:otherObject]; // 轉(zhuǎn)發(fā)消息 } else { [self doesNotRecognizeSelector:sel]; // 拋出異常 } }
KVO
當(dāng)我們?yōu)閷ο筇砑佑^察者后,Runtime 會在運行時創(chuàng)建這個對象所在類的子類,并且將該對象的 isa 指針指向這個子類,然后重寫監(jiān)聽屬性的 set 方法并在方法中調(diào)用 -willChangeValueForKey: 和 -didChangeValueForKey: 來通知觀察者,所以如果直接修改實例變量便不會觸發(fā)監(jiān)聽方法。當(dāng)移除觀察者后,Runtime 便會將這個子類刪除。
所以 isa 指針并不總是指向?qū)嵗龑ο笏鶎俚念?,也有可能指向一個中間類,所以不能依靠它來確定類型,而是應(yīng)該用 class 方法來確定實例對象的類。
關(guān)聯(lián)對象 (Associated Objects)
在 Category 中可以為類添加實例方法或類方法,但是不支持添加實例變量,所以即使我們在 Category 中為類添加了 property,也不能直接使用它,Runtime 可以解決這個問題,我們只需要定義一個指針,然后通過 objc_setAssociatedObject 方法將指針與對象進行關(guān)聯(lián)并指定內(nèi)存管理方式,數(shù)據(jù)以 KeyValue 的形式存儲在一個 HashMap 里。
Objc 中的類和對象都是結(jié)構(gòu)體,Category 也是這樣,定義的方法和屬性在結(jié)構(gòu)體中的存儲,并在運行時按倒序添加到主類中(添加的方法會放在方法列表的上面),所以如果添加的方法與原類中的一樣,那么在調(diào)用此方法時,優(yōu)先找到的便是我們添加的這個方法。如果有多個 Category 添加同樣名稱的方法,那么這些方法在方法列表中的順序取決于他們的編譯順序,也就是這些 Category 文件在 Compile Sources 中的順序。
@interface NSObject (JC) @property (nonatomic, copy) NSString *ID; @end @implementation NSObject (JC) static const void *IDKey; - (NSString *)ID { return objc_getAssociatedObject(self, &IDKey); } - (void)setID:(NSString *)ID { objc_setAssociatedObject(self, &IDKey, ID, OBJC_ASSOCIATION_COPY_NONATOMIC); } @end
AOP(Method Swizzling)
我們可以通過繼承、Category、AOP 方式來擴展類的功能。
- 繼承比較適合在設(shè)計底層代碼架構(gòu)時使用,不適當(dāng)?shù)氖褂脮尨a看起來很啰嗦,并且增加維護難度。
- Category 適合為現(xiàn)有類添加方法。
- 當(dāng)需要修改現(xiàn)有類的方法并且拿不到源碼時,繼承和 AOP 都能解決問題,但是用 AOP 來解決代碼耦合度更低。其實就算能拿到源碼,往往直接去改源碼也不是個好辦法。
在 Objective-C 中,可以通過 Method Swizzling 技術(shù)來實現(xiàn) AOP,下面我們通過交換兩個方法的實現(xiàn)代碼來向已存在的方法中添加其它功能。
#import <objc/runtime.h> @implementation UIViewController (Tracking) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class aClass = [self class]; SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(swizzled_viewWillAppear:); Method originalMethod = class_getInstanceMethod(aClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); // 如果要對類方法進行交換,使用下面注釋的代碼 // Class aClass = object_getClass((id)self); // // Method originalMethod = class_getClassMethod(aClass, originalSelector); // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector); // 交換兩個方法的實現(xiàn) // 防止 aClass 不存在 originalSelector,所以添加一下試試,但指向地址為新方法地址 BOOL didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { // 添加成功,說明 aClass 不存在 originalSelector,所以替換 swizzledSelector 的 IMP 為 originalMethod,實質(zhì)上它們都指向 swizzledMethod class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 添加失敗,說明 aClass 存在 originalSelector,直接交換 method_exchangeImplementations(originalMethod, swizzledMethod); } }); } #pragma mark - Method Swizzling // 由于方法實現(xiàn)已經(jīng)被交換,所以系統(tǒng)在調(diào)用 viewWillAppear: 時,實際上會調(diào)用 swizzled_viewWillAppear: - (void)swizzled_viewWillAppear:(BOOL)animated { // 下面代碼表面上看起來會引起遞歸調(diào)用,由于函數(shù)實現(xiàn)已經(jīng)被交換,實際上會調(diào)用 viewWillAppear: [self swizzled_viewWillAppear:animated]; // 在原有基礎(chǔ)上添加其它功能(寫日志等) } @end
使用 Method Swizzling 需要注意下面幾個問題
- 需要在 +load 方法中執(zhí)行 Method Swizzling,+initialize 方法有可能不會被調(diào)用
- 避免父類與子類同時 hook 父類的某方法,避免不了時至少要保證不在 +load 方法中執(zhí)行 super.load(),否則父類中的 +load 方法會被執(zhí)行兩次
- 需要在 dispatch_once 中執(zhí)行,避免因多線程等問題倒致的偶數(shù)次交換后失效的問題
- 如果你用了 swizzled_viewWillAppear 作為方法名,那么如果你引用的第三方 SDK 中也用了這個方法名來做方法交換,那會造成方法的遞歸調(diào)用,所以你最好換一個不太會被重復(fù)使用的方法名,例如 mx_swizzled_viewWillAppear
- 即便使用 mx_swizzled_viewWillAppear 盡量避免了與第三方庫或自己項目中別的地方對 viewWillAppear 交換倒致的遞歸調(diào)用問題,仍然會存在調(diào)用順序問題,解決辦法就是在 Build Phases 中調(diào)整類文件的順序
其它
我們可以通過 Runtime 特性來獲得類的所有屬性名稱和類型,然后再通過 KVC 將 JSON 中的值填充給該類的對象。還可以在程序運行時為類添加方法或替換方法從而使對象能夠更靈活的根據(jù)需要來選擇實現(xiàn)方法??傊?Runtime 庫就象一堆積木,只要發(fā)揮想象力便能實現(xiàn)各種各樣的功能,但前提是你需要了解它。
以上就是Objective-C Runtime 開發(fā)示例詳解的詳細內(nèi)容,更多關(guān)于Objective-C Runtime 開發(fā)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IOS collectionViewCell防止復(fù)用的兩種方法
這篇文章主要介紹了IOS collectionViewCell防止復(fù)用的兩種方法的相關(guān)資料,需要的朋友可以參考下2016-11-11iOS AVPlayer切換播放源實現(xiàn)連續(xù)播放和全屏切換的方法
這篇文章主要給大家介紹了關(guān)于iOS中AVPlayer切換播放源實現(xiàn)連續(xù)播放和全屏切換的方法,文中給出了詳細的示例代碼供大家參考學(xué)習(xí),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-05-05- 本文總結(jié)了2016年比較嚴(yán)重的iOS漏洞(可用于遠程代碼執(zhí)行或越獄),希望能夠?qū)Υ蠹乙苿影踩矫娴墓ぷ骱脱芯繋硪恍椭?/div> 2016-12-12
iOS swift 總結(jié)NavigationController出現(xiàn)問題及解決方法
這篇文章主要介紹了iOS swift 總結(jié)NavigationController出現(xiàn)問題及解決方法的相關(guān)資料,需要的朋友可以參考下2016-12-12iOS基礎(chǔ)知識之@property 和 Ivar 的區(qū)別
這篇文章主要介紹了iOS基礎(chǔ)知識之@property 和 Ivar 的區(qū)別介紹,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-08-08最新評論