1. Introduction

In this section, we will take a look at what the Runtime can do and what it can do in my actual development. The following are several specific application scenarios to discuss with you, hoping to deepen the understanding of Runtime and proficiently use it in projects through practical application.

2. Application scenarios of Runtime

2.1 Add gradient color to UIView

  • Before that, let’s first get familiar with the following points:
// Set the associated object
/* Parameter description: object: adds associated objects. Key: indicates the key used to store associated objects. Value: indicates the associated objects
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
// Get the associated object
/* Parameter description: object: adds the associated object. Key: Obtains the associated object by using the storage key. */
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
// Remove the associated object
objc_removeAssociatedObjects(id _Nonnull object)
Copy the code
  • I am not going to say much about memory management here. A table stolen from the Internet, from here, can very well illustrate the memory management mode corresponding to each enumeration.
Memory strategy Properties of modified describe
OBJC_ASSOCIATION_ASSIGN @property (assign) or @property (unsafe_unretained) Specifies a weak reference to an associated object.
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) @property (nonatomic, strong) specifies a strong reference to the associated object that cannot be used atomically.
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) Specifies a copy reference to an associated object that cannot be used atomically.
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) Specifies a strong reference to an associated object that can be used atomically.
OBJC_ASSOCIATION_COPY @property (atomic, copy) Specifies a copy reference to an associated object that can be used atomically.
  • Are you ready? Now we’re going to get into the real thing. Before we do that, let’s think carefully, can we add gradients to the UIView we’re using so far? I don’t think so. YeahiOSThere’s only one in the systemCAGradientLayerYou can set gradients. butCAGradientLayerThere’s a problem with not being able to respond to events, so how do we add one to a normal UIViewCAGradientLayer?
  • First createUIView+GradientThis is a category, where we can set properties and methods for a category
@interface UIView (Gradient)
// The gradient of the CAGradientLayer requires these parameters
@property (nonatomic, strong) NSArray *colors;
@property (nonatomic, strong) NSArray *locations;
@property (nonatomic, assign) CGPoint startPoint;
@property (nonatomic, assign) CGPoint endPoint;

// Create a gradient UIView
+ (UIView *)gradientViewWithColors:(NSArray *)colors locations:(NSArray *)locations startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint;
// Set the CAGradientLayer parameters
- (void)setGrandientBackgroundWithColors:(NSArray *)colors locations:(NSArray *)locations startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint;

@end
Copy the code
  • inUIViewThere is no interiorCAGradientLayerAre we going to create one for UIView? You don’t have to, because everyUIViewThey all have one that comes with themlayerObject, we can replace itCALayerSwitch toCAGradientLayer, through the following methods:
// default is [CALayer class]. Used when creating the underlying layer for the view.
@property(class.nonatomic.readonly) Class layerClass;                    
Copy the code

Add the following method to the UIView+ gradient. m file so that the UIView layer is actually CAGradientLayer and we can set the Gradient to the CAGradientLayer at creation time.

+ (Class)layerClass {
    return [CAGradientLayer class];
}
Copy the code
  • Implement the creation of a gradientUIViewObject and properties that set the gradient color
+ (UIView *)gradientViewWithColors:(NSArray *)colors locations:(NSArray *)locations startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint {
    UIView *view = [[self alloc] init];
    [view setGrandientBackgroundWithColors:colors locations:locations startPoint:startPoint endPoint:endPoint];
    return view;
}

- (void)setGrandientBackgroundWithColors:(NSArray *)colors locations:(NSArray *)locations startPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint {
    NSMutableArray *tempColors = [NSMutableArray new];
    for (UIColor *color in colors) {
        [tempColors addObject:(__bridge id)color.CGColor];
    }
    // Add a property to a class, which implements the corresponding setter and getter methods
    self.colors = tempColors;
    self.locations = locations;
    self.startPoint = startPoint;
    self.endPoint = endPoint;
}

- (void)setColors:(NSArray *)colors {
    // Give self an associative object of the color array, save the associative object when the setter method is called above, and set the gradient
    objc_setAssociatedObject(self, @selector(colors), colors, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // We need to cast it to CAGradientLayer because the class method above already specified that we return CAGradientLayer
    CAGradientLayer *gLayer = (CAGradientLayer *)self.layer;
    gLayer.colors = colors;
}

- (NSArray *)colors {
    // Use the _cmd key, which is essentially @selector(colors), to get the array of colors that are stored above
    return objc_getAssociatedObject(self, _cmd);
}

// The following is the same as the above color array
- (void)setLocations:(NSArray *)locations {
    objc_setAssociatedObject(self, @selector(locations), locations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    CAGradientLayer *gLayer = (CAGradientLayer *)self.layer;
    gLayer.locations = locations;
}

- (NSArray *)locations {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setStartPoint:(CGPoint)startPoint {
    objc_setAssociatedObject(self, @selector(startPoint), [NSValue valueWithCGPoint:startPoint], OBJC_ASSOCIATION_ASSIGN);
    CAGradientLayer *gLayer = (CAGradientLayer *)self.layer;
    gLayer.startPoint = startPoint;
}

- (CGPoint)startPoint {
    NSValue *pointValue = objc_getAssociatedObject(self, _cmd);
    return pointValue.CGPointValue;
}

- (void)setEndPoint:(CGPoint)endPoint {
    objc_setAssociatedObject(self, @selector(endPoint), [NSValue valueWithCGPoint:endPoint], OBJC_ASSOCIATION_ASSIGN);
    CAGradientLayer *gLayer = (CAGradientLayer *)self.layer;
    gLayer.endPoint = endPoint;
}

- (CGPoint)endPoint {
    NSValue *pointValue = objc_getAssociatedObject(self, _cmd);
    return pointValue.CGPointValue;
}
Copy the code
  • So now we have our classification, and we can create a gradient at willviewCome out, no need to beviewLet’s add another one up hereCAGradientLayerBelow is the result after creation
UIView *gradientView = [UIView gradientViewWithColors:@[[UIColor redColor], [UIColor greenColor]] locations:@[@0The @1] startPoint:CGPointMake(0.0.5) endPoint:CGPointMake(1.0.5)];
gradientView.frame = CGRectMake(10.100, self.view.bounds.size.width - 20.200);
[self.view addSubview:gradientView];
gradientView.backgroundColor = [UIColor yellowColor];
Copy the code

2.2 Message Forwarding

Objc_msgSend (void /* id self, SEL op… Void /* struct objc_super *super, SEL op… */) is a method that calls the parent class. When we use [obj sayHello], the compiler converts the message to objc_msgSend(obj, sayHello), and the internal Runtime process looks something like this.

  • First of all byobjtheisaThe pointer found itclass;
  • inclassthecacheInternal search to see if there is a previous call, if there is a direct find cache function to execute, if not to go toclassthemethod listLook for;
  • If theclassWas not found insayHelloContinue throughsuperclassFind the parent class in the parent classmethod listIn the search;
  • If found, execute the corresponding functionIMP(If the message cannot be found, the message forwarding process will be entered later);

If it is not found within the class or along the inheritance tree, it enters the message forwarding process, and the system provides three final opportunities to intercept and process the unimplemented method before the message forwarding fails.

  • Dynamic method parsing
  • Alternate receiver
  • Complete message forwarding

2.2.1 Dynamic Method analysis

When OC does not find SEL corresponding IMP, it will call +resolveInstanceMethod or +resolveClassMethod to give you the opportunity to intercept these two places. You can dynamically specify an IMP execution function for the current SEL inside these two methods. So you can avoid a crash.

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector: @selector(sayHello:) withObject:@"Ha ha"];
}
// This method can intercept unimplemented methods and dynamically add an IMP execution function to the current object
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sayHello:)) {
        // "v@:@" is explained in the previous section
        class_addMethod(self, sel, (IMP)mySayHello, "v@:@");
        // If a method is added dynamically, return YES or NO
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void mySayHello(id self, SEL _cmd, id obj) {
    NSLog(@"sel:%@, obj:%@", NSStringFromSelector( _cmd), obj);
}
// Print the result
2021-10-21 11:51:34.232107+0800 abffff[20246:412330[sel:sayHello, obj: HahaCopy the code

Using the dynamic parsing above, we add an execution function to the current control, waiting for the next call [self performSelector: @selector(sayHello:) withObject:@” haha “), because inside the class you can find mySayHello by @selector(sayHello:) SEL and execute it directly, +resolveInstanceMethod will not be called.

2.2.2 Alternate Receiver

When + resolveInstanceMethod or + resolveClassMethod method without dynamic method to add objects, whether the two method returns YES or NO, the target object will be called – forwardingTargetForSelector, This method gives us the opportunity to forward a currently unimplemented method to another object.

// Define a Person class implementing the sayHello method
@interface Person: NSObject
@end

@implementation Person
- (void)sayHello:(NSString *)hi {
    NSLog(@"person sayHello:%@", hi);
}
@end
// This method is implemented in the viewController and forwarded to the alternate object Person for processing
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayHello:)) {
        // Let Peron receive the message without implementing the method itself
        return [Person new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
// Print the following result
2021-10-21 15:05:05101455.+0800 abffff[35193:518268[Person sayHello: HahaCopy the code

2.2.3 Complete message forwarding

If the message has not been processed in the previous two steps, the full message forwarding mechanism will be entered. First will send – (NSMethodSignature *) methodSignatureForSelector: (SEL * * * *) aSelector this method needs to return to SEL of unrealized function signatures, if returns nil, The classic unrecognized selector send to… And in fact is the Runtime call – doesNotRecognizeSelector:; If the function invocation is returned, the Runtime creates an NSInvocation internally and calls – (void)forwardInvocation (NSInvocation *)anInvocation to the CREATED NSInvocation. In this method, we can forward the message to the specified object via invokeWithTarget.

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // It is ok to return NO and YES
    return NO;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayHello:)) {
        // v@:@ Refer to the previous section
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    Person *p = [Person new];
    if ([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    } else {
        return[self doesNotRecognizeSelector:sel]; }}Copy the code

The above are the three process of message forwarding. The message forwarding process is referenced here. In step 3, THE NSInvocation and NSMethodSignature can also be used to send messages to the specified object, passing multiple arguments compared to the performSelector.

2.3 Dictionary model conversion

MJExtension and YYModel are two frameworks commonly used in iOS development to convert dictionary models to each other. In fact, both frameworks use Runtime internally to assign values to dictionary keys and object attribute names. The core method is in NSObject classification. Let’s emulate these two frameworks to implement a dictionary model interchange to improve our Runtime proficiency.

- (instancetype)iyc_modelWithDict:(NSDictionary *)dict {
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList(self.class, &count);
    for (int i = 0; i < count; i++) {
        objc_property_t property = propertyList[i];
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
        NSString *propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(property)];
        // MJExtension uses propertyAttributes as an example of the propertyAttributes type
        // propertyAttributes are separated by commas
        NSArray *attributes = [propertyAttributes componentsSeparatedByString:@","];
        // The first string is the type of the attribute
        NSString *propertyTypeStr = attributes[0];
        if ([propertyTypeStr isEqualToString:@"T@\"NSString\""]) {
            / / string
            if (dict[propertyName] != nil) {
                [self setValue:dict[propertyName] forKey:propertyName];
            }
        } else if ([propertyTypeStr isEqualToString:@"Tq"]) {
            / / plastic
            if (dict[propertyName] != nil) {
                [self setValue:dict[propertyName] forKey:propertyName];
            }
        } else if ([propertyTypeStr isEqualToString:@"TB"]) {
            // BOOL
            if (dict[propertyName] != nil) {
                [self setValue:dict[propertyName] forKey:propertyName];
            }
        } else if ([propertyTypeStr isEqualToString:@"Tf"]) {
            / / floating point
            if (dict[propertyName] != nil) {
                [self setValue:dict[propertyName] forKey:propertyName];
            }
        } else if ([propertyTypeStr isEqualToString:@"Td"]) {
            // double
            if(dict[propertyName] ! = nil) { [self setValue:dict[propertyName] forKey:propertyName]; } } NSLog(@"propertyName:%@, propertyAttributes:%@", propertyName, propertyAttributes);
    }
    free(propertyList);
    return  self;
}
Copy the code

Let’s make a Person class and see if our own methods work.

@interface Person: NSObject
@property (nonatomic, copy) NSString *name;                 / /! < name
@property (nonatomic, assign) NSInteger age;                / /! The < age
@property (nonatomic, assign) BOOL isMarried;               / /! < Married or not
@property (nonatomic, assign) float weight;                 / /! The < weight
@property (nonatomic, assign) double money;                 / /! The < money
@end

@implementation Person
- (void)sayHello:(NSString *)hi {
    NSLog(@"person sayHello:%@", hi);
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    NSDictionary *d3 = @{@"name": @"Sony"The @"age": @"42"The @"isMarried": @1The @"weight": @"133.4"The @"money": @"99442.44"};
    Person *p = [[Person alloc] init];
    [p iyc_modelWithDict:d3];
    NSLog(@"p.name=%@\np.age=%zd\np.isMarried=%d\np.weight=%f\np.money=%f", p.name, p.age, p.isMarried, p.weight, p.money);
}

// The following information is displayed
2021-10-22 15:09:59.431060+0800 abffff[5299:1068425] propertyName:name, propertyAttributes:T@"NSString",C,N,V_name
2021-10-22 15:09:59.431273+0800 abffff[5299:1068425] propertyName:age, propertyAttributes:Tq,N,V_age
2021-10-22 15:09:59.431401+0800 abffff[5299:1068425] propertyName:isMarried, propertyAttributes:TB,N,V_isMarried
2021-10-22 15:09:59.431582+0800 abffff[5299:1068425] propertyName:weight, propertyAttributes:Tf,N,V_weight
2021-10-22 15:09:59.431739+0800 abffff[5299:1068425] propertyName:money, propertyAttributes:Td,N,V_money
2021-10-22 15:09:59.431876+0800 abffff[5299:1068425] p.name=Sony
p.age=42
p.isMarried=1
p.weight=133.399994
p.money=99442.440000
Copy the code

We can see from the printed information that our customized dictionary model has been successful; But T@”NSString”,C,N,V_name, what exactly does that mean? I’ve posted a link to the official website, which explains exactly what each character string stands for.

  1. Property string separated by,;
  2. The first property is the type of the property, the second is the memory management, the third is atomicity, and the fourth is the name of the property.

For example, T@”NSString”,C,N,V_name, the property is NSString, the modifier is copy, the atomicity is nonatomic, and the property name is name.

2.4 Hooks in iOS development

Hook, English called hook, comes from Windows program development. When I heard about hooks, they were so fancy, I didn’t know what they were. In iOS, a hook is a function inserted during message delivery that changes the order of execution of the message or delays the execution of the message, so that another method is called before one function or method is executed, and then the original execution path is returned. This is well explained by the picture below.

In iOS, the only way to do this is throughMethod SwizzleUnder normal circumstances, one SEL corresponds to one IMP, as shown in the figure below.

We can use method_exchangeImplementations to swap two SELS for IMPs

Without further ado, the code is a little more direct. We have simulated the no-trace embedding feature of the viewController by uploading the current page’s embedding parameters to the background during the viewController’s lifetime. Think about it, if we have 100 pages inside our APP, and each viewController has four or five lifecycle functions, do we want to add our buried upload parameters to each of those lifecycle functions? Here you can hook it. Create a new viewController class UIViewController+Hook.

@implementation UIViewController (Hook)
+ (void)load {
    [super load];
    // Get two SEL corresponding IMPs, then swap.
    Method m1 = class_getInstanceMethod(self, @selector(viewDidLoad));
    Method m2 = class_getInstanceMethod(self, @selector(iyc_viewDidLoad));
    method_exchangeImplementations(m1, m2);
}
- (void)iyc_viewDidLoad {
    /** We can add some functionality here, such as burrow uploads, so we don't have to manually add burrows to each page's viewDidLoad, because all the controller calls to viewDidLoad will pass through here, so we can handle */ in one place
    [self uploadTrackWithParams:@{@"pageName": @"Home page"}];
    // iyc_viewDidLoad is called here, because iyc_viewDidLoad points to the IMP before the swap, which is the original viewDidLoad method
    [self iyc_viewDidLoad];
}
- (void)uploadTrackWithParams:(NSDictionary *)params {
    NSLog(@"Start uploading the burial site.");
}
Copy the code

So every time we call viewDidLoad, we’re actually doing iyc_viewDidLoad first, so inside this method we can pass the parameters that we need to upload to the background, and then at the end of the method we call iyc_viewDidLoad, Because then iyc_viewDidLoad SEL is pointing to the IMP of the swapped viewDidLoad, which is the original execution method, back to the original execution line. This allows us to add a buried upload function to each controller’s lifecycle hook without changing the original code.

3. The end

That’s all for the basics and basic uses of the Runtime, along with references to many other blogs. Thank you. In fact, there are many more applications of the Runtime. Here are just a few simple tips to help you remember how to use the Runtime.