Runtime - 基於isa-swizzling實現訊息監聽,擴充套件響應式框架

TangentW發表於2017-10-18

前言

在上一篇文章《函數語言程式設計 - 實現響應式框架》中,我實現了一個非常簡單小巧的函式式響應式框架,並對它做了與Cocoa相關的一些擴充套件,比如支援將UIControl的使用者觸發事件以及Notification轉換成響應式的流,供我們進行流轉換以及訂閱。在其中有一個比較重要的擴充套件我還沒有去實現,那就是對Runtime的適配。通過對Runtime的適配,我們就能監聽某個方法的呼叫,包括協議的方法(儘管此時方法還沒有被實現)。由於此部分技術更多是偏向於Runtime,所以這篇文章並不歸納於“函數語言程式設計”範疇。本文的重點將放在對Objective-CRuntime的探討上,在最後才將響應式框架與適配好的Runtime結合起來。

這篇文章的主要思想及實現,參考自ReactiveCocoa

目標

我們的目標就是要完成一件事:監聽,並且針對的是方法的呼叫(訊息傳送)監聽:每次方法被呼叫時,我們就能收到監聽的回撥,並且得到當時傳入方法中的引數值。其能帶給我們的價值是非常大的,我們能在方法不改變其原有的工作流程、返回資料的基礎上,對方法進行特定的擴充套件。

這是一種非常暗黑的魔法,它能給方法動態提供了一種二次的實現,不僅只是單純地增添方法的功能,還能做到從實現埋點統計、Log輸出到AOP(面向切面程式設計)的運用,甚至它還能實現我們自己的KVO(監聽setter方法)。

另外,我們還需要考慮的一點是代理模式。Cocoa中代理模式使用得非常頻繁,不過這種模式使用起來並不是十分簡便:我們需要讓特定類去實現代理介面,並提供相應抽象方法的實現,而通過對Runtime進行適配後,我們不需要代理類去做相關實現就能對相應的代理抽象方法進行呼叫監聽。

最終效果

我們來看下適配後的最終效果。這裡我們展示的是通過閉包回撥的情況,而關聯了響應式框架的效果在後面才提到。

Objective-C

[self listen: @selector(touchesBegan:withEvent:) in: nil with: ^(NSArray * _Nonnull parameters) {
    NSLog(@"Touches began");
}];

[self listen: @selector(tableView:didSelectRowAtIndexPath:) in: @protocol(UITableViewDelegate) with: ^(NSArray * _Nonnull parameters) {
    if (parameters.count != 2) return;
    NSIndexPath *indexPath = parameters[1];
    NSLog(@"Did selected row %ld", (long)indexPath.row);
}];複製程式碼

Swift

// 普通方法呼叫監聽
listen(#selector(ViewController.touchesBegan(_:with:)), in: nil) { _ in
    print("Touches began")
}

// 代理方法呼叫監聽
// 注:此時self所屬類並不需要實現`tableView(_:didSelectRowAt:)`方法
listen(#selector(UITableViewDelegate.tableView(_:didSelectRowAt:)), in: UITableViewDelegate.self) { parameters in
    // parameters則為呼叫特定方法時所傳入的引數,以`[Any]`陣列的形式呈現
    guard
        parameters.count == 2,
        let indexPath = parameters[1] as? IndexPath
    else { return }
    print("Did selected row \(indexPath.row)")
}
// 設定`TableView`代理
_tableView.delegate = self複製程式碼

原理

監聽基本原理

首先來說下監聽回撥的基本原理,下面是一張原理示意圖:

監聽基本原理
監聽基本原理

我們自頂向下看,首先我們可以通過performSelector或者[obj message]的形式用特定的Selector向物件傳送訊息,此時Runtime系統會進入訊息派發的流程中,如果我們什麼都不做,訊息派發流程最終就會找到相應的方法實現,從而呼叫實現得到結果返回,若我們要監聽方法的呼叫,則需要在訊息派發的過程中動點手腳,將方法呼叫的事件從裡面回撥出來。

訊息派發原理

當訊息傳送時,在訊息派發的流程中我們不僅需要呼叫原來相應的方法實現,還需要回撥資訊來通知外界。要實現這個過程,我們用到了一個十分巧妙的方法。

這裡我列出方法的步驟:

  1. 建立一個新的方法,指定一個新的Selector給它,並將原始方法(被監聽的方法)的Implementation(實現)賦予給這個新的方法。

    補充:在OC中,方法由Selector(選擇器)以及Implementation(實現)構成。在OC中傳送訊息,首先是利用選擇器找到對應的方法,將其中的方法實現提取出來,然後再呼叫方法實現從而得到最終結果。

  2. 將原始方法的方法實現替換成_objc_msgForward

    補充:OC中方法實現的型別其實都是函式指標,而_objc_msgForward的型別也是函式指標,它的作用就是觸發完整的訊息轉發過程。當我們利用方法選擇器往物件傳送訊息時,Runtime會先後在方法快取列表、類物件方法列表、派生類物件及上層若干派生類物件的方法列表中查詢方法,若找到方法,即可提取方法實現進行呼叫,若最終依舊找不到方法,則執行時會直接呼叫_objc_msgForward,此時就進入訊息的轉發流程中。將原始方法的方法實現替換成_objc_msgForward,當我們用原始方法的Selector傳送訊息時,Runtime會直接進入訊息轉發流程。

  3. 重寫類物件的forwardInvocation:方法,在裡面做兩件事情:①提取方法呼叫時傳入的引數,向外界回撥。②將訊息轉發給在第一步建立的新方法。

    補充:因為在第二步中,我們已經將原始方法的實現替換成了_objc_msgForward,當我們通過原始方法的選擇器傳送訊息時,會走方法轉發的流程,由於我們並沒有重寫resolveInstanceMethod:forwardingTargetForSelector:方法,所以最終我們會進入forwardInvocation:方法,而在裡面我們需要做的,是向外界回撥資訊以及將訊息轉發到新方法中,從而呼叫原始的方法實現。

訊息派發原理1
訊息派發原理1

訊息派發原理2
訊息派發原理2

方法重寫原理

我們要實現對訊息派發流程的修改,則需要對forwardInvocation:方法進行重寫,然而,這種重寫並不是像平時一樣簡單地在類或擴充套件中提供自己重寫實現後的方法,我們需要使用到一個技術:isa-swizzling

我們知道,Runtime是利用isa-swizzling技術來實現KVO的,在執行時重寫相應屬性的setter方法,而我們這裡也是利用isa-swizzling去重寫forwardInvocation:方法。

isa-swizzling
isa-swizzling

如上圖所示,isa-swizzling其實就是在執行時動態建立了一箇中間層的類物件,這個類物件繼承自舊的類物件(isa),然後重寫相應的方法,最後,執行時將例項中的類物件(isa)替換成這個中間層類物件。經過isa-swizzling處理過的例項,它的isa已經替換了,所以此時向它傳送訊息,方法首先是在新的中間層類物件的方法列表中進行查詢,若中間層類物件重寫了方法,Runtime則會呼叫這個重寫方法的實現。

這個丟擲一個問題:

為什麼監聽方法呼叫不直接使用method-swizzling(方法交換)?

為了監聽方法的呼叫,上面所述的各種原理略為複雜,而使用method-swizzling依然能夠向方法提供二次的實現、監聽方法呼叫,且這樣子寫起來更為簡便,為什麼不直接使用方法交換,還要進行isa-swizzling跟方法重寫?這裡有兩個原因:

  1. 方法交換實現起來其實並不簡便,對於每一個需要二次實現的方法,我們都要編寫一套交換的程式碼(包括新的交換方法),若方法量多,程式碼則會變得冗長(當然可以考慮使用巨集)。而基於上述的isa-swizzling原理,我們可以將一系列操作封裝起來,最終只需通過一個方法即可完成對方法的呼叫監聽。
  2. 防止類物件被汙染:我們使用方法交換,需要在原始的類物件中進行操作,如一般來說我們會重寫類物件的load方法,在裡面實現方法交換的相關邏輯,這樣做會對原始類物件造成汙染。舉個例子,若我們通過方法交換重寫了類A中的方法α,那麼在整個專案中,當我們向所有屬於類A的例項傳送方法α的訊息,最終都會走重寫後的實現。假如我們只需要監聽某個指定例項的方法呼叫時,我們對這個方法進行方法交換,那麼最終改變行為的不只是這個指定的例項,而是所有與此同類的例項。為此,我們需要利用isa-swizzling,它做的,就是修改某個指定例項的類物件(isa),到最後,改變行為的只是這個例項,因為此時它已經不屬於原本所屬舊類物件的例項了。

實現

介面

因為這裡涉及較多Runtime的API,所以整個實現我使用的是Objective-C語言。
整個實現是在NSObject的擴充套件NSObject+Runtime裡面的,所以這裡我建立了這個擴充套件,並提供以下的介面:

typedef void (^MessageDidSendCallback) (NSArray * _Nonnull);

@interface NSObject (Runtime)

- (void)listen:(nonnull SEL)selector in:(nullable Protocol *)protocol with:(nonnull MessageDidSendCallback)callback;

@end複製程式碼
  • MessageDidSendCallback就是方法呼叫回撥的block型別,它具有一個陣列型別的引數,作用是傳遞方法被呼叫時傳入的引數。
  • 我們給NSObject新增了監聽方法,方法有三個引數,第一個就是我們需要監聽的方法的選擇器,第二個則為可空的協議型別(當指定的方法屬於協議方法,傳入協議物件,反之則傳nil),第三個就是監聽回撥的block。當指定的方法被呼叫時,callback就會被呼叫,我們可以從它的陣列引數中獲取到方法被呼叫時傳入的引數值。

介面實現

- (void)listen:(SEL)selector in:(Protocol *)protocol with:(MessageDidSendCallback)callback {
    SEL runtimeSelector = _modifySelector(selector);
    // 引用閉包
    objc_setAssociatedObject(self, runtimeSelector, callback, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // isa-swizzling
    Class interlayerClass = _swizzleClass(self);
    Method originalMethod = class_getInstanceMethod(interlayerClass, selector);
    IMP originalImplementation = method_getImplementation(originalMethod);

    // 判斷是否具有該方法
    // 如果沒有,試圖在指定的協議中尋找
    if (!originalMethod) {
        if (!protocol) return;
        struct objc_method_description des = protocol_getMethodDescription(protocol, selector, YES, YES);
        if (!des.name)
            des = protocol_getMethodDescription(protocol, selector, NO, YES);
        if (des.types)
            class_addMethod(interlayerClass, selector, _objc_msgForward, des.types);
    }
    // 如果原始方法沒有做替換
    // 則將原始方法的實現改為_objc_msgForward
    else if (originalImplementation != _objc_msgForward) {
        const char *typeEncoding = method_getTypeEncoding(originalMethod);
        class_addMethod(interlayerClass, runtimeSelector, originalImplementation, typeEncoding);
        class_replaceMethod(interlayerClass, selector, _objc_msgForward, typeEncoding);
    }
}複製程式碼

從上往下看,首先呼叫_modifySelector在原來的方法選擇器基礎上經過修飾,得到新的方法的方法選擇器,修飾的過程比較簡單:

// 用於在原有的基礎上標示Selector以及中間層類物件的名字,便於區分
static NSString * const _prefixName = @"_Runtime_";

// 修飾Selector,返回經過字首名拼接的Selector
static SEL _Nonnull _modifySelector(SEL _Nonnull selector) {
    NSString *originalName = NSStringFromSelector(selector);
    return NSSelectorFromString([_prefixName stringByAppendingString:originalName]);
}複製程式碼

我們通過拼接一個修飾字串到原來選擇器的字串上,並利用這個拼接後的字串通過NSSelectorFromString轉換成新的選擇器。

接下來通過使用這個新的方法選擇器作為key,將listen方法傳入的block物件設定成例項自己的關聯物件,目的是維持callback block的存活,以及便於我們後期在forwardInvocation:方法中獲取到這個block。

接著,我們通過函式_swizzleClass進行isa-swizzling操作(後面會說到),這個函式返回的是剛建立好的中間層類物件。

拿到這個中間層類物件後,我們就能夠在裡面以指定的舊方法為基礎,建立一個新的方法(利用舊的方法實現以及新的方法選擇器),並將舊方法的方法實現替換成_objc_msgForward。此時操作的是中間層類物件,所以不會汙染到原本的類物件。在這裡需要注意的有:

  • 若在之前我們已經為指定的方法進行實現替換了,我們就沒必要再重複操作。
  • 若在這個中間層類物件中找不到舊方法,而listen傳入的協議並不為空,則在協議裡面查詢方法,若協議中確實具有此方法,那我們就動態往中間層類物件中新增這個協議方法,且方法的實現為_objc_msgForward。此時這個協議方法並不做任何事,它的作用只為了在這個方法被呼叫時傳送回撥。

isa-swizzling

// 關聯物件Key,是否已經存在中間層類物件
static void *_interlayerClassExist = &_interlayerClassExist;

// isa-swizzling
static Class _Nullable _swizzleClass(id _Nonnull self) {
    Class originalClass = object_getClass(self);
    // 如果在之前已經替換了isa,則只需直接返回
    if ([objc_getAssociatedObject(self, _interlayerClassExist) boolValue])
        return originalClass;

    Class interlayerClass;

    Class presentClass = [self class];
    // 若之前沒有手動替換過isa,但是兩種方式獲取到的Class不同
    // 說明此物件在之前被動態地替換isa,(可能是涉及到了KVO)
    // 這時候我們使用的中間層類物件就不需要動態建立一個了,直接使用之前動態建立的就行
    if (presentClass != originalClass) {
        // 重寫方法
        _swizzleForwardInvocation(originalClass);
        _swizzleRespondsToSelector(originalClass);
        _swizzleMethodSignatureForSelector(originalClass);

        interlayerClass = originalClass;
    }
    else {
        const char *interlayerClassName = [_prefixName stringByAppendingString:NSStringFromClass(originalClass)].UTF8String;
        // 首先判斷Runtime中是否已經註冊過此中間層類
        // 若沒有註冊,則動態建立中間層類並且重寫其中的指定方法,最後進行註冊
        interlayerClass = objc_getClass(interlayerClassName);
        if (!interlayerClass) {
            // 基於原始的類物件建立新的中間層類物件
            interlayerClass = objc_allocateClassPair(originalClass, interlayerClassName, 0);
            if (!interlayerClass) return nil;

            // 重寫方法
            _swizzleForwardInvocation(interlayerClass);
            _swizzleRespondsToSelector(interlayerClass);
            _swizzleMethodSignatureForSelector(interlayerClass);
            _swizzleGetClass(interlayerClass, presentClass);

            // 註冊中間層類物件
            objc_registerClassPair(interlayerClass);
        }
    }
    // isa替換
    object_setClass(self, interlayerClass);
    objc_setAssociatedObject(self, _interlayerClassExist, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return interlayerClass;
}複製程式碼

我們以_interlayerClassExist作為key,為物件設定了一個布林型別的關聯物件值,用於標示我們在之前是否已經為此物件進行過isa-swizzling,若isa已經替換過,則直接用object_getClass返回中間層類物件。

接下來做的事情比較微妙,我們比較了使用getClass方法以及object_getClass獲取到的兩個類物件,這樣做的目的是為了判別這個物件在之前是否也在其他地方進行過isa-swzzling,因為object_getClass獲取到的類物件是實際的類物件(isa),而getClass方法可能已經被重寫了,獲取到的類物件可能是虛假的,其中,最典型的isa-swizzling莫過於KVO了。當我們判別到物件在之前已經進行過isa-swizzling了,我們就沒必要再自己建立一箇中間層類物件了,直接使用現有的就行,反正也不會汙染到原本的類物件。當我們判別到物件在之前沒有進行過isa-swizzling,我們就需要手動建立一箇中間層類物件,這個類物件繼承自原來的類物件,且名字也是在舊類物件之上進行稍微的修飾。

接著,我們就對中間層類物件的某些方法進行重寫,這裡主要有四個方法需要重寫:

  • forwardInvocation: 重寫的原因在上面的原理中已經提到。
  • respondsToSelector: 考慮到中間層類物件中雖然沒有實現指定的方法,但是在我們傳入的協議中確實找到了它,所以已經在中間層類物件動態新增了,此時我們就需要重寫responseToSelector,使得我們通過responseToSelector能夠得知物件已經實現了此方法。舉個例子,我們平時呼叫代理方法時,總會加上一句判斷:

     if ([_delegate respondsToSelector: @selector(XXX)]) {
         [_delegate XXX];
     }複製程式碼

    若我們只是簡單地為中間層類物件動態新增協議的方法,卻沒有重寫respondsToSelector,那個此時這個方法也不可能會被呼叫。

  • methodSignatureForSelector: 為了能讓動態新增的方法也能通過此方法獲取到方法簽名,我們需要重寫。
  • getClass: 為了欺騙表層、欺騙世界,我們需要重寫此方法,讓這個方法返回的類物件非實際的中間層類物件,而是虛假的舊類物件。

重寫getClass

// 混淆getClass方法
static void _swizzleGetClass(Class _Nonnull class, Class _Nonnull expectedClass) {
    SEL selector = @selector(class);
    Method getClassMethod = class_getInstanceMethod(class, selector);
    id newImp = ^(id self) {
        return expectedClass;
    };
    class_replaceMethod(class, selector, imp_implementationWithBlock(newImp), method_getTypeEncoding(getClassMethod));
}複製程式碼

可以看到,我們重寫方法使用的不是方法交換技術,而是直接通過class_replaceMethod,將新的方法實現替換進方法中,而新的方法實現我們將利用block來建立。這裡需要注意的是,通過imp_implementationWithBlock函式,我們可以利用block建立方法實現,而這個block的型別有所約束:返回型別跟方法實現的一樣,而在引數中,第一個引數必須為id型別,代表此時傳送訊息的例項,後面緊接著的是方法的實際引數。而方法實現的型別中,前兩個引數為idSEL,代表傳送訊息的例項以及選擇器,後面才接方法的實際引數。

重寫respondsToSelector

// 混淆respondsToSelector方法
static void _swizzleRespondsToSelector(Class _Nonnull class) {
    SEL originalSelector = @selector(respondsToSelector:);
    Method method = class_getInstanceMethod(class, originalSelector);
    BOOL (*originalImplementation)(id, SEL, SEL) = (void *)method_getImplementation(method);
    id newImp = ^(id self, SEL selector) {
        Method method = class_getInstanceMethod(class, selector);
        if (method && method_getImplementation(method) == _objc_msgForward) {
            if (objc_getAssociatedObject(self, _modifySelector(selector)))
                return YES;
        }
        return originalImplementation(self, originalSelector, selector);
    };
    class_replaceMethod(class, originalSelector, imp_implementationWithBlock(newImp), method_getTypeEncoding(method));
}複製程式碼

通過method_getImplementation函式,我們可以直接獲取到原本的方法實現,方法實現的型別為函式指標,這讓我們可以在後面直接呼叫它。

中間判斷的意義是:此方法是中間層類物件動態建立的,因為此時方法可能是類沒有實現但協議宣告瞭,若此時例項對這個方法有進行監聽,respondsToSelector則返回YES,反之則返回NO,因為這個動態新增的方法只是為了實現方法呼叫的監聽回撥,既然例項沒有對其進行監聽,那麼respondsToSelector直接返回NO就行。

重寫methodSignatureForSelector

// 混淆methodSignatureForSelector方法
static void _swizzleMethodSignatureForSelector(Class _Nonnull class) {
    SEL msfsSelector = @selector(methodSignatureForSelector:);
    Method method = class_getInstanceMethod(class, msfsSelector);
    id newIMP = ^(id self, SEL selector) {
        Method method = class_getInstanceMethod(class, selector);
        if (!method) {
            struct objc_super super = {
                self,
                class_getSuperclass(class)
            };
            NSMethodSignature *(*sendToSuper)(struct objc_super *, SEL, SEL) = (void *)objc_msgSendSuper;
            return sendToSuper(&super, msfsSelector, selector);
        }
        return [NSMethodSignature signatureWithObjCTypes: method_getTypeEncoding(method)];
    };
    class_replaceMethod(class, msfsSelector, imp_implementationWithBlock(newIMP), method_getTypeEncoding(method));
}複製程式碼

這新的實現中,我們先通過傳入的方法選擇器找到對應的方法,若此時方法存在,我們通過方法的型別編碼建立方法簽名並返回,若此時方法不存在,我們則呼叫父類的methodSignatureForSelector方法。我們知道,在平時我們通過[super XXX]向父類傳送訊息時,最終都是轉換成objc_msgSendSuper的形式,而此時我們是使用block來建立新的方法實現,不能使用到[super XXX]這種形式,所以我們直接通過objc_msgSendSuper來向父類傳送訊息。

重寫forwardInvocation

// 混淆forwardInvocation方法
static void _swizzleForwardInvocation(Class _Nonnull class) {
    SEL fiSelector = @selector(forwardInvocation:);
    Method fiMethod = class_getInstanceMethod(class, fiSelector);
    void (*originalFiImp)(id, SEL, NSInvocation *) = (void *)method_getImplementation(fiMethod);
    id newFiImp = ^(id self, NSInvocation *invocation) {
        SEL runtimeSelector = _modifySelector(invocation.selector);
        MessageDidSendCallback callback = (MessageDidSendCallback)objc_getAssociatedObject(self, runtimeSelector);
        if (!callback) {
            if (originalFiImp)
                originalFiImp(self, fiSelector, invocation);
            else
                [self doesNotRecognizeSelector: invocation.selector];
        } else {
            if ([self respondsToSelector: runtimeSelector]) {
                invocation.selector = runtimeSelector;
                [invocation invoke];
            }
            callback(_getArguments(invocation));
        }
    };
    class_replaceMethod(class, fiSelector, imp_implementationWithBlock(newFiImp), method_getTypeEncoding(fiMethod));
}複製程式碼

在新的實現中,我們通過NSInvocation轉發訊息到新的方法中,方式就是直接設定invocation的selector為新方法的選擇器。另外,我們通過_getArguments函式從invocation中把傳入方法的引數提取出來,傳入在之前設定好的callback block關聯物件進行呼叫,這樣我們就能夠將方法呼叫回撥到外界了。

我們看下_getArguments函式:

static NSArray * _Nonnull _getArguments(NSInvocation * _Nonnull invocation) {
    NSUInteger count = invocation.methodSignature.numberOfArguments;
    // 除去開頭的兩個引數(id, SEL),代表例項自己以及方法的選擇器
    NSMutableArray *arr = [NSMutableArray arrayWithCapacity:count - 2];
    for (NSUInteger i = 2; i < count; i ++)
        [arr addObject:_getArgument(invocation, i)];
    return arr;
}

// 獲取引數,copy from `ReactiveCocoa`
static id _Nonnull _getArgument(NSInvocation * _Nonnull invocation, NSUInteger index) {
    const char *argumentType = [invocation.methodSignature getArgumentTypeAtIndex:index];

#define RETURN_VALUE(type) \
else if (strcmp(argumentType, @encode(type)) == 0) {\
type val = 0; \
[invocation getArgument:&val atIndex:index]; \
return @(val); \
}

    // Skip const type qualifier.
    if (argumentType[0] == 'r') {
        argumentType++;
    }

    if (strcmp(argumentType, @encode(id)) == 0
        || strcmp(argumentType, @encode(Class)) == 0
        || strcmp(argumentType, @encode(void (^)(void))) == 0
        ) {
        __unsafe_unretained id argument = nil;
        [invocation getArgument:&argument atIndex:index];
        return argument;
    }
    RETURN_VALUE(char)
    RETURN_VALUE(short)
    RETURN_VALUE(int)
    RETURN_VALUE(long)
    RETURN_VALUE(long long)
    RETURN_VALUE(unsigned char)
    RETURN_VALUE(unsigned short)
    RETURN_VALUE(unsigned int)
    RETURN_VALUE(unsigned long)
    RETURN_VALUE(unsigned long long)
    RETURN_VALUE(float)
    RETURN_VALUE(double)
    RETURN_VALUE(BOOL)
    RETURN_VALUE(const char *)
    else {
        NSUInteger size = 0;
        NSGetSizeAndAlignment(argumentType, &size, NULL);
        NSCParameterAssert(size > 0);
        uint8_t data[size];
        [invocation getArgument:&data atIndex:index];

        return [NSValue valueWithBytes:&data objCType:argumentType];
    }
}複製程式碼

_getArguments函式中,我們獲取到引數數量,並過濾掉前面兩個引數(因為前面兩個引數分別代表呼叫方法的例項以及此方法的選擇器,並不是實際傳入方法的引數),再一個個通過_getArgument函式獲取到最終的值。_getArgument函式的機理有些複雜,我直接拷貝自ReactiveCocoa的原始碼。

完整程式碼

#import "NSObject+Runtime.h"
#import <objc/runtime.h>
#import <objc/message.h>

static SEL _Nonnull _modifySelector(SEL _Nonnull selector);
static Class _Nullable _swizzleClass(id _Nonnull self);

@implementation NSObject (Runtime)

- (void)listen:(SEL)selector in:(Protocol *)protocol with:(MessageDidSendCallback)callback {
    SEL runtimeSelector = _modifySelector(selector);
    // 引用閉包
    objc_setAssociatedObject(self, runtimeSelector, callback, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // isa-swizzling
    Class interlayerClass = _swizzleClass(self);
    Method originalMethod = class_getInstanceMethod(interlayerClass, selector);
    IMP originalImplementation = method_getImplementation(originalMethod);

    // 判斷是否具有該方法
    // 如果沒有,試圖在指定的協議中尋找
    if (!originalMethod) {
        if (!protocol) return;
        struct objc_method_description des = protocol_getMethodDescription(protocol, selector, YES, YES);
        if (!des.name)
            des = protocol_getMethodDescription(protocol, selector, NO, YES);
        if (des.types)
            class_addMethod(interlayerClass, selector, _objc_msgForward, des.types);
    }
    // 如果原始方法沒有做替換
    // 則將原始方法的實現改為_objc_msgForward
    else if (originalImplementation != _objc_msgForward) {
        const char *typeEncoding = method_getTypeEncoding(originalMethod);
        class_addMethod(interlayerClass, runtimeSelector, originalImplementation, typeEncoding);
        class_replaceMethod(interlayerClass, selector, _objc_msgForward, typeEncoding);
    }
}

@end

#pragma mark - Private 私有
// 用於在原有的基礎上標示Selector以及中間層類物件的名字,便於區分
static NSString * const _prefixName = @"_Runtime_";

// 關聯物件Key,是否已經存在中間層類物件
static void *_interlayerClassExist = &_interlayerClassExist;

// 獲取引數
static id _Nonnull _getArgument(NSInvocation * _Nonnull invocation, NSUInteger index) {
    const char *argumentType = [invocation.methodSignature getArgumentTypeAtIndex:index];

#define RETURN_VALUE(type) \
else if (strcmp(argumentType, @encode(type)) == 0) {\
type val = 0; \
[invocation getArgument:&val atIndex:index]; \
return @(val); \
}

    // Skip const type qualifier.
    if (argumentType[0] == 'r') {
        argumentType++;
    }

    if (strcmp(argumentType, @encode(id)) == 0
        || strcmp(argumentType, @encode(Class)) == 0
        || strcmp(argumentType, @encode(void (^)(void))) == 0
        ) {
        __unsafe_unretained id argument = nil;
        [invocation getArgument:&argument atIndex:index];
        return argument;
    }
    RETURN_VALUE(char)
    RETURN_VALUE(short)
    RETURN_VALUE(int)
    RETURN_VALUE(long)
    RETURN_VALUE(long long)
    RETURN_VALUE(unsigned char)
    RETURN_VALUE(unsigned short)
    RETURN_VALUE(unsigned int)
    RETURN_VALUE(unsigned long)
    RETURN_VALUE(unsigned long long)
    RETURN_VALUE(float)
    RETURN_VALUE(double)
    RETURN_VALUE(BOOL)
    RETURN_VALUE(const char *)
    else {
        NSUInteger size = 0;
        NSGetSizeAndAlignment(argumentType, &size, NULL);
        NSCParameterAssert(size > 0);
        uint8_t data[size];
        [invocation getArgument:&data atIndex:index];

        return [NSValue valueWithBytes:&data objCType:argumentType];
    }
}

static NSArray * _Nonnull _getArguments(NSInvocation * _Nonnull invocation) {
    NSUInteger count = invocation.methodSignature.numberOfArguments;
    // 除去開頭的兩個引數(id, SEL),代表例項自己以及方法的選擇器
    NSMutableArray *arr = [NSMutableArray arrayWithCapacity:count - 2];
    for (NSUInteger i = 2; i < count; i ++)
        [arr addObject:_getArgument(invocation, i)];
    return arr;
}

// 修飾Selector,返回經過字首名拼接的Selector
static SEL _Nonnull _modifySelector(SEL _Nonnull selector) {
    NSString *originalName = NSStringFromSelector(selector);
    return NSSelectorFromString([_prefixName stringByAppendingString:originalName]);
}

// 混淆forwardInvocation方法
static void _swizzleForwardInvocation(Class _Nonnull class) {
    SEL fiSelector = @selector(forwardInvocation:);
    Method fiMethod = class_getInstanceMethod(class, fiSelector);
    void (*originalFiImp)(id, SEL, NSInvocation *) = (void *)method_getImplementation(fiMethod);
    id newFiImp = ^(id self, NSInvocation *invocation) {
        SEL runtimeSelector = _modifySelector(invocation.selector);
        MessageDidSendCallback callback = (MessageDidSendCallback)objc_getAssociatedObject(self, runtimeSelector);
        if (!callback) {
            if (originalFiImp)
                originalFiImp(self, fiSelector, invocation);
            else
                [self doesNotRecognizeSelector: invocation.selector];
        } else {
            if ([self respondsToSelector: runtimeSelector]) {
                invocation.selector = runtimeSelector;
                [invocation invoke];
            }
            callback(_getArguments(invocation));
        }
    };
    class_replaceMethod(class, fiSelector, imp_implementationWithBlock(newFiImp), method_getTypeEncoding(fiMethod));
}

// 混淆getClass方法
static void _swizzleGetClass(Class _Nonnull class, Class _Nonnull expectedClass) {
    SEL selector = @selector(class);
    Method getClassMethod = class_getInstanceMethod(class, selector);
    id newImp = ^(id self) {
        return expectedClass;
    };
    class_replaceMethod(class, selector, imp_implementationWithBlock(newImp), method_getTypeEncoding(getClassMethod));
}

// 混淆respondsToSelector方法
static void _swizzleRespondsToSelector(Class _Nonnull class) {
    SEL originalSelector = @selector(respondsToSelector:);
    Method method = class_getInstanceMethod(class, originalSelector);
    BOOL (*originalImplementation)(id, SEL, SEL) = (void *)method_getImplementation(method);
    id newImp = ^(id self, SEL selector) {
        Method method = class_getInstanceMethod(class, selector);
        if (method && method_getImplementation(method) == _objc_msgForward) {
            if (objc_getAssociatedObject(self, _modifySelector(selector)))
                return YES;
        }
        return originalImplementation(self, originalSelector, selector);
    };
    class_replaceMethod(class, originalSelector, imp_implementationWithBlock(newImp), method_getTypeEncoding(method));
}

// 混淆methodSignatureForSelector方法
static void _swizzleMethodSignatureForSelector(Class _Nonnull class) {
    SEL msfsSelector = @selector(methodSignatureForSelector:);
    Method method = class_getInstanceMethod(class, msfsSelector);
    id newIMP = ^(id self, SEL selector) {
        Method method = class_getInstanceMethod(class, selector);
        if (!method) {
            struct objc_super super = {
                self,
                class_getSuperclass(class)
            };
            NSMethodSignature *(*sendToSuper)(struct objc_super *, SEL, SEL) = (void *)objc_msgSendSuper;
            return sendToSuper(&super, msfsSelector, selector);
        }
        return [NSMethodSignature signatureWithObjCTypes: method_getTypeEncoding(method)];
    };
    class_replaceMethod(class, msfsSelector, imp_implementationWithBlock(newIMP), method_getTypeEncoding(method));
}

// isa-swizzling
static Class _Nullable _swizzleClass(id _Nonnull self) {
    Class originalClass = object_getClass(self);
    // 如果在之前已經替換了isa,則只需直接返回
    if ([objc_getAssociatedObject(self, _interlayerClassExist) boolValue])
        return originalClass;

    Class interlayerClass;

    Class presentClass = [self class];
    // 若之前沒有手動替換過isa,但是兩種方式獲取到的Class不同
    // 說明此物件在之前被動態地替換isa,(可能是涉及到了KVO)
    // 這時候我們使用的中間層類物件就不需要動態建立一個了,直接使用之前動態建立的就行
    if (presentClass != originalClass) {
        // 重寫方法
        _swizzleForwardInvocation(originalClass);
        _swizzleRespondsToSelector(originalClass);
        _swizzleMethodSignatureForSelector(originalClass);

        interlayerClass = originalClass;
    }
    else {
        const char *interlayerClassName = [_prefixName stringByAppendingString:NSStringFromClass(originalClass)].UTF8String;
        // 首先判斷Runtime中是否已經註冊過此中間層類
        // 若沒有註冊,則動態建立中間層類並且重寫其中的指定方法,最後進行註冊
        interlayerClass = objc_getClass(interlayerClassName);
        if (!interlayerClass) {
            // 基於原始的類物件建立新的中間層類物件
            interlayerClass = objc_allocateClassPair(originalClass, interlayerClassName, 0);
            if (!interlayerClass) return nil;

            // 重寫方法
            _swizzleForwardInvocation(interlayerClass);
            _swizzleRespondsToSelector(interlayerClass);
            _swizzleMethodSignatureForSelector(interlayerClass);
            _swizzleGetClass(interlayerClass, presentClass);

            // 註冊中間層類物件
            objc_registerClassPair(interlayerClass);
        }
    }
    // isa替換
    object_setClass(self, interlayerClass);
    objc_setAssociatedObject(self, _interlayerClassExist, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return interlayerClass;
}複製程式碼

自此我們就完成了對Runtime的適配,現在我們可以直接利用listen方法來實現對指定方法的監聽了。

擴充套件響應式框架

有了前面的基礎,將響應式框架向Runtime擴充套件起來就十分簡單了。

由於前面我編寫響應式框架是使用的是Swift語言,所以這裡我也是通過Swift語言進行擴充套件:

extension NSObject {
    func listen(_ selector: Selector, in proto: Protocol? = nil) -> Signal<[Any]> {
        return Signal { [weak self] observer in
            self?.listen(selector, in: proto, with: observer.sendNext)
        }
    }
}複製程式碼

現在,我們可以把玩一下經過Runtime擴充套件後的響應式框架了:

 listen(#selector(UITableViewDelegate.tableView(_:didSelectRowAt:)), in: UITableViewDelegate.self)
     .map { $0[1] as! IndexPath }
     .map { [weak self] in self?._data[$0.row] }
     .subscribe(next: { [weak self] in
         guard let uid = $0 else { return }
         self?.navigationController?.pushViewController(MyViewController(uid: uid), animated: true)
     })

_tableView.delegate = self複製程式碼

參考

文章主要思想及實現參考自ReactiveCocoa,實現的程式碼可能存在某些缺漏或不足,若大家有興趣可直接檢視ReactiveCocoa的原始碼:ReactiveCocoa

本文純屬個人見解,若大家發現文章部分有誤,歡迎在評論區提出。

相關文章