IOS開發Objective-C Runtime使用示例詳解

前言

Runtime 是使用 C 和匯編實現的運行時代碼庫,Objective-C 中有很多語言特性都是通過它來實現。瞭解 Runtime 開發可以幫助我們更靈活的使用 Objective-C 這門語言,我們可以將程序功能推遲到運行時再去決定怎麼做,還可以利用 Runtime 來解決項目開發中的一些設計和技術問題,使開發過程更加具有靈活性。

一些關鍵字

  • self:類的隱藏參數變量,指向當前調用方法的對象
  • super:是編譯器的標示符,通過 super 調用方法會被翻譯成 objc_msgSendSuper(self, _cmd,…)
  • SEL:以方法名為內容的 C 字符串
  • IMP:指向方法實現的函數指針
  • id:指向類對象或實例對象的指針
  • isa:為 id 對象所屬類型 (objc_class),Objc 中的繼承就是通過 isa 指針找到 objc_class,然後再通過 super_class 去找對應的父類
  • metaclass:在 Objc 中,類本身也是對象,實例對象的 isa 指向它所屬的類,而類對象的 isa 指向元類 (metaclass),元類的 isa 直接指向根元類,根元類的isa指向它自己,它們之間的關系如下圖所示。

消息傳遞 (Messaging)

Objective-C 對於調用對象的某個方法這種行為叫做給對象發送消息,實際上就是沿著它的 isa 指針去查找真正的函數地址。下面我們來瞭解一下這個過程:

我們寫一個給對象發送消息的代碼

[array insertObject:obj atIndex:5];

編譯器首先會將上面代碼翻譯成這種樣子

objc_msgSend(array, @selector(insertObject:atIndex:), obj, 5);

系統在運行時會通過 array 對象的 isa 指針找到對應的 class(如果是給類發消息,則找到的是metaclass),然後在 class 的 cache 方法列表中用 SEL 去找對應 method,如果找不到便去 class 的方法列表中去找,如果在方法列表中也找不對對應 method 時,便沿著繼承體系繼續向上查找,找到後將 method 放入 cache,以便下次能快速定位,然後再去執行 method 的 IMP,找不到時系統便報錯:unrecognized selector sent to insertObject:atIndex:

Runtime 提供瞭三種方法避免因為找不到方法而崩潰

當找不到方法實現時,Runtime 會先發送 +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 方法的最後一個參數用來指定所添加方法的參數及返回值,叫 Type Encodings。

如果 resolve 方法返回 NO,Runtime 會發送 -forwardingTargetForSelector: 消息,允許我們將消息轉發給能處理它的其它對象。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx:)){
        return otherObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

當 -forwardingTargetForSelector: 返回 nil 時,Runtime 會發送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。我們可以選擇忽略消息、拋出異常、將消息轉由當前對象或其它對象的任意消息來處理。

//根據 SEL 生成 NSInvocation 對象,然後再由 -forwardInvocation: 方法進行轉發。
- (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]; // 轉發消息
    } 
    else {
        [self doesNotRecognizeSelector:sel]; // 拋出異常
    }
}

KVO

當我們為對象添加觀察者後,Runtime 會在運行時創建這個對象所在類的子類,並且將該對象的 isa 指針指向這個子類,然後重寫監聽屬性的 set 方法並在方法中調用 -willChangeValueForKey: 和 -didChangeValueForKey: 來通知觀察者,所以如果直接修改實例變量便不會觸發監聽方法。當移除觀察者後,Runtime 便會將這個子類刪除。

所以 isa 指針並不總是指向實例對象所屬的類,也有可能指向一個中間類,所以不能依靠它來確定類型,而是應該用 class 方法來確定實例對象的類。

關聯對象 (Associated Objects)

在 Category 中可以為類添加實例方法或類方法,但是不支持添加實例變量,所以即使我們在 Category 中為類添加瞭 property,也不能直接使用它,Runtime 可以解決這個問題,我們隻需要定義一個指針,然後通過 objc_setAssociatedObject 方法將指針與對象進行關聯並指定內存管理方式,數據以 KeyValue 的形式存儲在一個 HashMap 裡。

Objc 中的類和對象都是結構體,Category 也是這樣,定義的方法和屬性在結構體中的存儲,並在運行時按倒序添加到主類中(添加的方法會放在方法列表的上面),所以如果添加的方法與原類中的一樣,那麼在調用此方法時,優先找到的便是我們添加的這個方法。如果有多個 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 方式來擴展類的功能。

  • 繼承比較適合在設計底層代碼架構時使用,不適當的使用會讓代碼看起來很囉嗦,並且增加維護難度。
  • Category 適合為現有類添加方法。
  • 當需要修改現有類的方法並且拿不到源碼時,繼承和 AOP 都能解決問題,但是用 AOP 來解決代碼耦合度更低。其實就算能拿到源碼,往往直接去改源碼也不是個好辦法。

在 Objective-C 中,可以通過 Method Swizzling 技術來實現 AOP,下面我們通過交換兩個方法的實現代碼來向已存在的方法中添加其它功能。

#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);
 		// 交換兩個方法的實現
 		// 防止 aClass 不存在 originalSelector,所以添加一下試試,但指向地址為新方法地址
        BOOL didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); 
        if (didAddMethod) { 
        	// 添加成功,說明 aClass 不存在 originalSelector,所以替換 swizzledSelector 的 IMP 為 originalMethod,實質上它們都指向 swizzledMethod
            class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); 
        } 
        else { 
         	// 添加失敗,說明 aClass 存在 originalSelector,直接交換
            method_exchangeImplementations(originalMethod, swizzledMethod); 
        } 
    }); 
} 
#pragma mark - Method Swizzling 
// 由於方法實現已經被交換,所以系統在調用 viewWillAppear: 時,實際上會調用 swizzled_viewWillAppear:
- (void)swizzled_viewWillAppear:(BOOL)animated { 
	// 下面代碼表面上看起來會引起遞歸調用,由於函數實現已經被交換,實際上會調用 viewWillAppear:
   [self swizzled_viewWillAppear:animated]; 
	// 在原有基礎上添加其它功能(寫日志等)
} 
@end

使用 Method Swizzling 需要註意下面幾個問題

  • 需要在 +load 方法中執行 Method Swizzling,+initialize 方法有可能不會被調用
  • 避免父類與子類同時 hook 父類的某方法,避免不瞭時至少要保證不在 +load 方法中執行 super.load(),否則父類中的 +load 方法會被執行兩次
  • 需要在 dispatch_once 中執行,避免因多線程等問題倒致的偶數次交換後失效的問題
  • 如果你用瞭 swizzled_viewWillAppear 作為方法名,那麼如果你引用的第三方 SDK 中也用瞭這個方法名來做方法交換,那會造成方法的遞歸調用,所以你最好換一個不太會被重復使用的方法名,例如 mx_swizzled_viewWillAppear
  • 即便使用 mx_swizzled_viewWillAppear 盡量避免瞭與第三方庫或自己項目中別的地方對 viewWillAppear 交換倒致的遞歸調用問題,仍然會存在調用順序問題,解決辦法就是在 Build Phases 中調整類文件的順序

其它

我們可以通過 Runtime 特性來獲得類的所有屬性名稱和類型,然後再通過 KVC 將 JSON 中的值填充給該類的對象。還可以在程序運行時為類添加方法或替換方法從而使對象能夠更靈活的根據需要來選擇實現方法。總之 Runtime 庫就象一堆積木,隻要發揮想象力便能實現各種各樣的功能,但前提是你需要瞭解它。

以上就是Objective-C Runtime 開發示例詳解的詳細內容,更多關於Objective-C Runtime 開發的資料請關註WalkonNet其它相關文章!

推薦閱讀: