首頁 > 軟體

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

2023-02-14 06:00:41

前言

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 開發的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com