preface

In my last article, Functional Programming – Implementing a Responsive Framework, I implemented a very simple and compact functional responsive framework and made some Cocoa related extensions to it, such as support for converting UIControl’s user-triggered events and Notifications into responsive flows. For streaming conversion and subscription. One of the more important extensions I haven’t implemented yet is the Runtime adaptation. By adapting the Runtime, we can listen for calls to certain methods, including protocol methods (even though the methods are not yet implemented). Because this part of the technique is more Runtime oriented, this article does not fall into the “functional programming” category. The focus of this article will be on the Discussion of Runtime in Objective-C, and at the end of the article will be to combine responsive frameworks with well-adapted Runtime.

The main idea and implementation of this article refer to ReactiveCocoa.

The target

Our goal is to do one thing: listen, and listen for method invocation (message sending) : every time a method is called, we get a callback from the listen and the values of the parameters in the method that was passed in at the time. The value of this is that we can extend a method specifically without changing its original workflow and returning data.

This is a very dark magic that provides a secondary implementation of method dynamics, not just to add functionality to methods, but to use everything from implementing buried statistics and Log output to AOP (aspect oriented programming), and even to implement our own KVO (listening on setter methods).

Another thing we need to consider is the proxy pattern. Cocoa proxy mode used very frequently, but this pattern is not very easy to use: we need to make a particular class to implement proxy interface, and provides the corresponding the realization of the abstract methods, and through the study of the adaptation of the Runtime, we don’t need the proxy class to do the related implementation of the corresponding agent abstraction method is invoked to monitor.

The final result

Let’s take a look at the end result. Here we show callbacks via closures, but the effects of associated reactive frameworks are discussed later.

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);
}];Copy the code

Swift

// Normal method call listener
listen(#selector(ViewController.touchesBegan(_:with:)), in: nil) { _ in
    print("Touches began")}// Proxy method calls listener
// Note that the class that self belongs to does not need to implement the 'tableView(_:didSelectRowAt:)' method
listen(#selector(UITableViewDelegate.tableView(_:didSelectRowAt:)), in: UITableViewDelegate.self) { parameters in
    // parameters are the parameters passed in when a particular method is called, represented as an array of '[Any]'
    guard
        parameters.count= =2.let indexPath = parameters[1] as? IndexPath
    else { return }
    print("Did selected row \(indexPath.row)")}// Set the 'TableView' proxy
_tableView.delegate = selfCopy the code

The principle of

Monitoring Principles

First, the basic principle of listening for callbacks is described below:

Monitoring Principles

So if we look at it from the top down, first of all we can send a message to an object with a particular Selector in the form of performSelector or [obj message], and then the Runtime system will go through the process of sending that message, and if we don’t do anything, The message dispatch process will eventually find the corresponding method implementation and call the implementation to return the result. If we want to listen to the method invocation, we need to do something in the message dispatch process to call back the event of the method invocation from inside.

Message Distribution Principle

When the message is sent, in the process of message distribution, we not only need to call the original corresponding method implementation, but also need to call back information to inform the outside world. To do this, we use a very clever method.

Here I list the steps of the method:

  1. Create a new method and specify a newSelectorGive it and place the original method (the method being listened on)ImplementationGive this new method.

    Note: In OC, methods are made up of selectors and implementations. When sending messages in OC, the first step is to use the selector to find the corresponding method, extract the method implementation, and then call the method implementation to get the final result.

  2. Replace the method implementation of the original method with_objc_msgForward.

    Note: the OC method implementations are all function Pointers, and _objc_msgForward is also a function pointer, which triggers the entire message forwarding process. When we use the method selector to send a message to an object, the Runtime looks for a method in the method cache list, the method list of a class object, the method list of a derived class object, and the method list of several upper-level derived objects. If a method is found, the Runtime extracts the method implementation and calls it. The runtime calls _objc_msgForward directly, and then the message forwarding process enters. Replace the method implementation of the original method with _objc_msgForward, and when we send a message using the Selector of the original method, the Runtime directly enters the message forwarding process.

  3. Overrides the class objectforwardInvocation:Method, which does two things: (1) extract the parameters passed in the method call, callback to the outside. ② Forward the message to the new method created in step 1.

    Supplement: Because in step 2 we replaced the implementation of the original method with _objc_msgForward, when we send a message through the selector of the original method, we go through the method forward process, Because we don’t have to rewrite resolveInstanceMethod: and forwardingTargetForSelector: method, so we will end up into forwardInvocation: method, and we need to do, The original method implementation is invoked by calling back information to the outside world and forwarding the message to the new method.

Message sending Principle 1

Message distribution Principle 2

Method rewriting principle

The forwardInvocation: method needs to be rewritten. However, instead of simply providing the method in the class or extension, we use a technique called isa-swizzling.

As we know, the Runtime uses isa-swizzling to implement KVO, overriding the setter method for the property at Runtime, and we used isa-swizzling to override the forwardInvocation: method.

isa-swizzling

As shown in the figure above, ISa-Swizzling essentially creates a mid-tier class object at runtime that inherits from the old class object (ISA), overrides the corresponding methods, and finally, the runtime replaces the instance class object (ISA) with the mid-tier class object. A message is sent to the isa-Swizzling instance whose ISA has been replaced. The method is first looked up in the list of methods on the new mid-tier class object. If the mid-tier class object overwrites the method, the Runtime calls the implementation of the overridden method.

This raises the question:

Why don’t you just listen for method calls using method-Swizzling?

In order to listen for method calls, the principles described above are slightly more complicated, but using method-Swizzling can still provide a secondary implementation to a method, listen for method calls, and it is easier to write this way. Why not just use method swaps, but isa-Swizzling and method rewriting? There are two reasons for this:

  1. Method swapping is not easy to implement. For every method that needs to be implemented twice, we write a set of code for swapping (including new swapping methods), which can become verbose if there are too many methods (macros are certainly an option). And based on the aboveisa-swizzlingIn principle, we can encapsulate a series of operations, and finally only need a method to call the listening method.
  2. Prevents class objects from being contaminatedWe use method swaps, which need to operate on the original class object, as we would normally override the class objectloadMethod to implement the logic associated with method exchange, which would pollute the original class object. For example, if we override method α in class A via method exchange, then throughout the project, when we send A message for method α to all instances of class A, we end up with the overridden implementation. If we switch methods on a given instance when we only need to listen for method calls on that instance, then it’s not just that instance that ends up changing its behavior, but all of its peers. To do that, we need to useisa-swizzlingAll it does is modify the class object (ISA) of a given instance. In the end, only the instance changes its behavior because it no longer belongs to the instance of the old class object.

implementation

interface

Because there are a lot of Runtime apis involved, I used Objective-C for the entire implementation. The whole implementation is in the NSObject extension NSObject+Runtime, so here I created this extension and provided the following interface:

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

@interface NSObject (Runtime)

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

@endCopy the code
  • MessageDidSendCallbackBlock is the block type of the method call callback, which takes an array type argument and passes the argument passed to the method when it is called.
  • We giveNSObjectThe first is the selector of the method we want to listen on, the second is the nullable protocol type (if the specified method is a protocol method, pass protocol object, otherwise pass nil), and the third is the block that listens for the callback. When the specified method is called, the callback will be called, and we can get the value of the parameter passed in when the method was called from its array arguments.

Interface implementation

- (void)listen:(SEL)selector in:(Protocol *)protocol with:(MessageDidSendCallback)callback {
    SEL runtimeSelector = _modifySelector(selector);
    // Reference closure
    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);

    // Check whether the method exists
    // If not, try to find it in the specified protocol
    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);
    }
    // If the original method is not replaced
    // Change the implementation of the original method to _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); }}Copy the code

From the top, first call _modifySelector and modify the original method selector to get the method selector of the new method. The modification process is relatively simple:

// It is used to identify the Selector and the name of the middle layer class object in addition to the existing one to facilitate differentiation
static NSString * const _prefixName = @"_Runtime_";

// Decorates a Selector, returning a concatenated Selector with a prefix name
static SEL _Nonnull _modifySelector(SEL _Nonnull selector) {
    NSString *originalName = NSStringFromSelector(selector);
    return NSSelectorFromString([_prefixName stringByAppendingString:originalName]);
}Copy the code

We concatenate a modifier string to the original selector’s string and use the concatenated string to convert it to the new selector via NSSelectorFromString.

We then set the block object passed in by the listen method to the instance’s own association by using the new method selector as the key. The purpose is to keep the callback block alive and to get the block later in the forwardInvocation: method.

Next, we perform isa-Swizzling (described below) with the _swizzleClass function, which returns the newly created middle tier class object.

Once we have the middle tier class object, we can create a new method (using the old method implementation and the new method selector) based on the specified old method and replace the old method implementation with _objc_msgForward. In this case, we operate on the middle class object, so we don’t pollute the original class object. Here are some things to note:

  • If we have already implemented the substitution for the specified method before, we do not need to repeat the operation.
  • If the old method is not found in the middle tier class object, andlistenIf the protocol passed in is not empty, look for the method in the protocol. If the protocol does have this method, then we dynamically add the protocol method to the middle layer class object, and the implementation of the method is_objc_msgForward. The protocol method does nothing at this point, except to send a callback when the method is called.

isa-swizzling

// Whether the middle tier class object already exists
static void *_interlayerClassExist = &_interlayerClassExist;

// isa-swizzling
static Class _Nullable _swizzleClass(id _Nonnull self) {
    Class originalClass = object_getClass(self);
    // If isa has been replaced before, it simply returns
    if ([objc_getAssociatedObject(self, _interlayerClassExist) boolValue])
        return originalClass;

    Class interlayerClass;

    Class presentClass = [self class];
    // If you have not manually replaced isa before, the Class obtained by the two methods is different
    // Indicates that this object was previously dynamically replaced with isa, (possibly involving KVO).
    // The middle layer class does not need to be created dynamically
    if(presentClass ! = originalClass) {// Override the method
        _swizzleForwardInvocation(originalClass);
        _swizzleRespondsToSelector(originalClass);
        _swizzleMethodSignatureForSelector(originalClass);

        interlayerClass = originalClass;
    }
    else {
        const char *interlayerClassName = [_prefixName stringByAppendingString:NSStringFromClass(originalClass)].UTF8String;
        // First check whether the middle tier class has been registered in the Runtime
        // If not registered, the middle tier class is dynamically created and the specified methods are overridden, and finally registered
        interlayerClass = objc_getClass(interlayerClassName);
        if(! interlayerClass) {// Create a new middle tier class object based on the original class object
            interlayerClass = objc_allocateClassPair(originalClass, interlayerClassName, 0);
            if(! interlayerClass)return nil;

            // Override the method
            _swizzleForwardInvocation(interlayerClass);
            _swizzleRespondsToSelector(interlayerClass);
            _swizzleMethodSignatureForSelector(interlayerClass);
            _swizzleGetClass(interlayerClass, presentClass);

            // Register the middle tier class objectobjc_registerClassPair(interlayerClass); }}/ / isa replacement
    object_setClass(self, interlayerClass);
    objc_setAssociatedObject(self, _interlayerClassExist, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return interlayerClass;
}Copy the code

With _interlayerClassExist as the key, we set a Boolean associated object value to indicate whether we have previously performed Isa-swizzling on this object. If isa has been replaced, Object_getClass returns the middle tier class object directly.

The next thing we do isa little bit subtle, we compare the two class objects that we get using the getClass method and object_getClass, and the purpose of doing that is to determine if this object has previously had isa-swzzling elsewhere, Because object_getClass gets an actual class object (ISA), the getClass method may have been overridden and the received class object may be a fake. The most typical isa-swizzling is KVO. When we determine that the object has been isa-swizzling before, we do not need to create a middle layer class object, we just use the existing one, it will not pollute the original class object anyway. When we determine that the object has not been isa-Swizzling before, we need to manually create a middle tier class object that inherits from the original class object with a slightly modified name on top of the old class object.

Next, we will rewrite some methods of the middle layer class object. There are four methods that need to be overridden:

  • ForwardInvocation: the reason for override is mentioned in the principle above.
  • respondsToSelector: And given that we didn’t implement the method in the middle layer class object, but we did find it in the protocol that we passed in, so we’ve dynamically added it to the middle layer class object, we need to override responseToSelector, That lets us know by responseToSelector that the object has implemented this method. For example, we usually call a proxy method with a judgment:

     if ([_delegate respondsToSelector: @selector(XXX)]) {
         [_delegate XXX];
     }Copy the code

    If we had simply added the protocol method to the middle layer class object dynamically without overriding respondsToSelector, that method would not have been called at this point.

  • MethodSignatureForSelector: to be able to dynamically add method can also through this method to get the signature, we need to rewrite.
  • GetClass: To fool the surface layer, to fool the world, we need to override this method so that the class object returned by this method is not the actual middle layer class object, but a fake old class object.

Rewrite the getClass

// Obfuscate the getClass method
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));
}Copy the code

As you can see, instead of using the method swap technique, we override the method directly via class_replaceMethod, replacing the new method implementation that we will create with blocks. Note here that the imp_implementationWithBlock function allows us to create a method with a block type constraint: The return type is the same as the method implementation, and in the arguments, the first argument must be of type ID, representing the instance at which the message was sent, followed by the actual arguments of the method. In the type of method implementation, the first two parameters are ID and SEL, representing the instance and selector of the message to be sent, followed by the actual parameters of the method.

Rewrite respondsToSelector

// Obfuscates the respondsToSelector method
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));
}Copy the code

With the method_getImplementation function, we get the original method implementation directly, which is of type a function pointer, allowing us to call it directly later.

The meaning of intermediate judgment is: RespondsToSelector returns YES if the instance is listening for the method, and NO if the instance is not listening for the method, because the method is added dynamically just to implement the listening callback for the method call. And since the instance isn’t listening on it, then respondsToSelector just returns NO.

Rewrite methodSignatureForSelector

/ / confusion methodSignatureForSelector method
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));
}Copy the code

This new implementation, we first through the incoming method selector to find the corresponding method, if the methods, we pass type encoding method to create the method signature and return, if this method doesn’t exist, we call the parent class methodSignatureForSelector method. We know that when we send a message to the parent class via [super XXX], we end up converting it to objc_msgSendSuper. In this case, we use block to create a new method implementation, so we can’t use [super XXX]. So we send messages directly to the parent class via objc_msgSendSuper.

Rewrite forwardInvocation

// confuse the forwardInvocation method
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 ([selfrespondsToSelector: runtimeSelector]) { invocation.selector = runtimeSelector; [invocation invoke]; } callback(_getArguments(invocation)); }}; class_replaceMethod(class, fiSelector, imp_implementationWithBlock(newFiImp), method_getTypeEncoding(fiMethod));
}Copy the code

In the new implementation, we use the NSInvocation to forward messages to the new method by setting the Selector directly to the new method. In addition, we’ll be able to call the method callback to the outside world by extracting the method’s parameters from the invocation via the _getArguments function and calling the callback block from the previously set invocation object.

Let’s look at the _getArguments function:

static NSArray * _Nonnull _getArguments(NSInvocation * _Nonnull invocation) {
    NSUInteger count = invocation.methodSignature.numberOfArguments;
    // Remove the first two arguments (id, SEL), representing the instance itself and the method selector
    NSMutableArray *arr = [NSMutableArray arrayWithCapacity:count - 2];
    for (NSUInteger i = 2; i < count; i ++)
        [arr addObject:_getArgument(invocation, i)];
    return arr;
}

// Get parameters, 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); The \}// 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 [NSValuevalueWithBytes:&data objCType:argumentType]; }}Copy the code

In the _getArguments function, we take the number of arguments, filter out the first two arguments (because the first two represent the instance of the calling method and the method selector, not the actual method parameters passed in), and then get the final values one by one through the _getArgument function. The mechanism of _getArgument function is a bit complicated, I copied it directly from the source code of ReactiveCocoa.

The complete code

#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);
    // Reference closure
    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);

    // Check whether the method exists
    // If not, try to find it in the specified protocol
    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);
    }
    // If the original method is not replaced
    // Change the implementation of the original method to _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
// It is used to identify the Selector and the name of the middle layer class object in addition to the existing one to facilitate differentiation
static NSString * const _prefixName = @"_Runtime_";

// Whether the middle tier class object already exists
static void *_interlayerClassExist = &_interlayerClassExist;

// Get parameters
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); The \}// 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 [NSValuevalueWithBytes:&data objCType:argumentType]; }}static NSArray * _Nonnull _getArguments(NSInvocation * _Nonnull invocation) {
    NSUInteger count = invocation.methodSignature.numberOfArguments;
    // Remove the first two arguments (id, SEL), representing the instance itself and the method selector
    NSMutableArray *arr = [NSMutableArray arrayWithCapacity:count - 2];
    for (NSUInteger i = 2; i < count; i ++)
        [arr addObject:_getArgument(invocation, i)];
    return arr;
}

// Decorates a Selector, returning a concatenated Selector with a prefix name
static SEL _Nonnull _modifySelector(SEL _Nonnull selector) {
    NSString *originalName = NSStringFromSelector(selector);
    return NSSelectorFromString([_prefixName stringByAppendingString:originalName]);
}

// confuse the forwardInvocation method
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 ([selfrespondsToSelector: runtimeSelector]) { invocation.selector = runtimeSelector; [invocation invoke]; } callback(_getArguments(invocation)); }}; class_replaceMethod(class, fiSelector, imp_implementationWithBlock(newFiImp), method_getTypeEncoding(fiMethod));
}

// Obfuscate the getClass method
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));
}

// Obfuscates the respondsToSelector method
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));
}

/ / confusion methodSignatureForSelector method
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);
    // If isa has been replaced before, it simply returns
    if ([objc_getAssociatedObject(self, _interlayerClassExist) boolValue])
        return originalClass;

    Class interlayerClass;

    Class presentClass = [self class];
    // If you have not manually replaced isa before, the Class obtained by the two methods is different
    // Indicates that this object was previously dynamically replaced with isa, (possibly involving KVO).
    // The middle layer class does not need to be created dynamically
    if(presentClass ! = originalClass) {// Override the method
        _swizzleForwardInvocation(originalClass);
        _swizzleRespondsToSelector(originalClass);
        _swizzleMethodSignatureForSelector(originalClass);

        interlayerClass = originalClass;
    }
    else {
        const char *interlayerClassName = [_prefixName stringByAppendingString:NSStringFromClass(originalClass)].UTF8String;
        // First check whether the middle tier class has been registered in the Runtime
        // If not registered, the middle tier class is dynamically created and the specified methods are overridden, and finally registered
        interlayerClass = objc_getClass(interlayerClassName);
        if(! interlayerClass) {// Create a new middle tier class object based on the original class object
            interlayerClass = objc_allocateClassPair(originalClass, interlayerClassName, 0);
            if(! interlayerClass)return nil;

            // Override the method
            _swizzleForwardInvocation(interlayerClass);
            _swizzleRespondsToSelector(interlayerClass);
            _swizzleMethodSignatureForSelector(interlayerClass);
            _swizzleGetClass(interlayerClass, presentClass);

            // Register the middle tier class objectobjc_registerClassPair(interlayerClass); }}/ / isa replacement
    object_setClass(self, interlayerClass);
    objc_setAssociatedObject(self, _interlayerClassExist, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return interlayerClass;
}Copy the code

Now that we’ve adapted the Runtime, we can use the Listen method directly to listen to the specified method.

Extended responsive framework

With this foundation in place, extending the reactive framework to the Runtime is straightforward.

Since I wrote the reactive framework in Swift, I extend it in Swift as well:

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)
        }
    }
}Copy the code

Now we can play with the Runtime extended responsive framework:

 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 = selfCopy the code

reference

The main idea and implementation of the article reference from ReactiveCocoa, the code may have some deficiencies or omissions, if you are interested in ReactiveCocoa can directly view the source code: ReactiveCocoa.

This article is purely personal. If you find any errors in the article, feel free to post them in the comments section.