preface

RunTime I first encountered the concept back in ’17, when there was a requirement that clicking buttons (not all) should first check whether the user is authenticated (presumably). At that time, the boss of iOS said that he would write it, and then I recode his code, so I knew there was this thing. Later, I met many big guys in the interview and asked them. Today, I will summarize briefly.

RumTime API

Runtime is a C language API that encapsulates many dynamic functions. It can dynamically create objects, methods and properties, and modify the methods and properties of classes and objects while programs are running. Let’s start with a brief introduction to common apis, and then we’ll talk about actual combat.

Objc related

function instructions
objc_getClass Get the Class object
objc_allocateClassPair Create a class dynamically
objc_registerClassPair Registering a class
objc_disposeClassPair Destroying a class
objc_setAssociatedObject Associate an object with an instance object
objc_getAssociatedObject Gets the associated object of the instance object

The class related

function instructions
class_getSuperclass Access to the parent class
class_addIvar Add member variables dynamically
class_addProperty Dynamically add property methods
class_addMethod Dynamic addition method
class_replaceMethod Dynamic substitution method
class_getInstanceVariable Get instance variables
class_getClassVariable Get class variables
class_getInstanceMethod Get instance method
class_getClassMethod Get class methods
class_getMethodImplementation Gets the parent method implementation
class_getInstanceSize Getting instance size
class_copyMethodList Gets an array of methods for the class

The object related

function instructions
object_getClassName Gets the class name of the object
object_getClass Gets the class of the object
object_getIvar Gets the value of an object member variable
object_setIvar Sets the value of an object member variable

Method the related

function instructions
method_getName Get method name
method_getImplementation Get the implementation of the method
method_getTypeEncoding Gets the type encoding of the method
method_setImplementation The implementation of the set method
method_exchangeImplementations Implementation of replacement methods

Property related

function instructions
property_getName Get attribute name
property_getAttributes Gets the property list for the property

Ivar related

function instructions
ivar_getName Gets the member variable name
ivar_getOffset Get offset
ivar_getTypeEncoding Get type encoding

Protocol related

function instructions
protocol_getName Obtaining the protocol name
protocol_addProperty Protocol Adding Properties
protocol_getProperty Obtaining protocol attributes
protocol_copyPropertyList The property list name of the copy protocol

The runtime of actual combat

KVO

The full name of KVO is (key-value Observing), commonly known as “key-value monitoring”, which can be used to monitor the change of an object’s attribute Value.

[self.person addObserver:self
              forKeyPath:@"age"
                 options:NSKeyValueObservingOptionNew
                 context:nil];

Copy the code

Use the Runtime API to dynamically generate a subclass and have instance object’s ISA point to this new subclass.

KVC

KVC is key-value coding, which allows developers to access or assign values to properties of objects directly through their key names. There is no need to call an explicit access method, so the properties of an object can be accessed and modified dynamically at run time rather than determined at compile time.

 [textField setValue:[UIColor greenColor] forKeyPath:@"_placeholderLabel.textColor"];
Copy the code

The example above is to change the placeholder text color using KVC. Unfortunately, in iOS13, textfield is prohibited from obtaining and changing private attributes using KVC.

Dictionary model

In OC, dictionary to model is generally used by the third-party library MJExtension and YYModel.

The basic principle is to use the Runtime feature to retrieve all attributes in the model to traverse the dictionary to be transformed using KVC’s – (nullable id)valueForKeyPath:(NSString *)keyPath; And – (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; Method to retrieve the model attribute and use it as the corresponding key in the dictionary to retrieve its corresponding value and assign value to the model attribute.

Here’s a simple dictionary-to-model example

- (void)transformDict:(NSDictionary *)dict { Class class = self.class; // count: number of member variables unsigned int count = 0; Ivar *ivars = class_copyIvarList(class, &count); For (int I = 0; i < count; Ivar = ivars[I]; Ivar = ivars[I]; / / get a member variable names nsstrings * key = [nsstrings stringWithUTF8String: ivar_getName (ivar)]; Key = [key substringFromIndex:1]; Id value = dict[key]; If (value == nil) continue; if (value == nil) continue; if (value == nil) continue; // use KVC to set the dictionary value to the model [self setValue:value forKeyPath:key]; } // Free (ivars); }Copy the code

Automatic filing and unfiling

When we need to archive an object, we always make the class of the object comply with the NSCoding protocol, and then implement archiving and archiving methods.

The basic idea is to use Runtime to get instance member variables and then manipulate them through KVC.

// Archive - (void)encodeWithCoder:(NSCoder *)coder {// count: number of member variables unsigned int count = 0; Ivar *ivars = class_copyIvarList([self class], &count); For (int I = 0; i < count; Ivar = ivars[I]; Ivar = ivars[I]; / / get a member variable names nsstrings * key = [nsstrings stringWithUTF8String: ivar_getName (ivar)]; Id value = [self valueForKey:key]; // coder encodeObject:value forKey:key]; } // Free (ivars); }Copy the code
- (instancetype)initWithCoder:(NSCoder *)coder {self = [super init]; If (self) {// count: number of member variables unsigned int count = 0; Ivar *ivars = class_copyIvarList([self class], &count); For (int I = 0; i < count; Ivar = ivars[I]; Ivar = ivars[I]; / / get a member variable names nsstrings * key = [nsstrings stringWithUTF8String: ivar_getName (ivar)]; Id value = [coder decodeObjectOfClasses:[NSSet setWithObject:[self class]] forKey:key]; // KVC [self setValue:value forKey:key]; } // Free (ivars); } return self; }Copy the code

Method Swizzling

Method Swizzing changes the Method call at runtime by changing the mapping of the Method List of the selector Class. This is essentially an IMP (method implementation) that swaps the two methods. Abuse is not recommended.

+ (void)jj_MethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL { if (! CLS) NSLog(@" The exchange class passed cannot be empty "); OriMethod = class_getInstanceMethod(CLS, oriSEL); SwiMethod = class_getInstanceMethod(CLS, swizzledSEL); OriSEL = oriSEL; oriSEL = oriSEL; oriSEL = oriSEL; oriSEL = oriSEL; BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); If (success) {// CLS does not have an oriMethod, There is a parent class class_replaceMethod(CLS, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else{currentOriMethod is currentCLS. Method_exchangeImplementations (oriMethod, swiMethod); }}Copy the code

Global page statistics

Add a UIViewController class, load and dispatch_once_t to make sure it’s only loaded once, swap UIViewController viewWillAppear for swizz_viewWillAppear.

+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSEL = @selector(viewWillAppear:); SEL swizzledSEL = @selector(swizz_viewWillAppear:); / / exchange method [JJRuntimeTool jj_MethodSwizzlingWithClass: class oriSEL: originalSEL swizzledSEL: swizzledSEL]; }); }Copy the code
- (void)swizz_viewWillAppear:(BOOL)animated {self swizz_viewWillAppear:animated [self swizz_viewWillAppear:animated]; NSLog(@" %@", [self class]); }Copy the code

Prevent button multiple click events

So here we’re going to work with the classification UIControl, using the association object to add the properties. DelayInterval to control whether the button can be clicked for a few seconds before continuing to respond to events.

@interface UIControl (Swizzling) // Whether to ignore events @Property (nonatomic, assign) BOOL ignoreEvent; @property (**nonatomic, assign) NSTimeInterval delayInterval @endCopy the code

Set the property’s set and get methods.

- (void)setIgnoreEvent:(BOOL)ignoreEvent
{
    objc_setAssociatedObject(self, @"associated_ignoreEvent", @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
}

- (BOOL)ignoreEvent
{
    return [objc_getAssociatedObject(self, @"associated_ignoreEvent") boolValue];
}

- (void)setDelayInterval:(NSTimeInterval)delayInterval
{
    objc_setAssociatedObject(self, @"associated_delayInterval", @(delayInterval), OBJC_ASSOCIATION_ASSIGN);
}

- (NSTimeInterval)delayInterval
{
    return [objc_getAssociatedObject(self, @"associated_delayInterval") doubleValue];
}
Copy the code

The implementation here also implements delayed response events by exchanging the sendAction:to:forEvent: method.

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        Class class = [self class];
        SEL originalSEL = @selector(sendAction:to:forEvent:);
        SEL swizzledSEL = @selector(swizzl_sendAction:to:forEvent:);
        
        [JJRuntimeTool jj_MethodSwizzlingWithClass:class oriSEL:originalSEL swizzledSEL:swizzledSEL];

    });
}
Copy the code
- (void)swizzl_sendAction (SEL)action to (id)target forEvent (UIEvent *)event {// If the response is ignored, Return if (self.ignoreEvent) return; If (self.delayInterval > 0) {// Add delay, ignoreEvent is set to YES for interception. self.ignoreEvent = YES; // After delayInterval seconds, Set ignoreEvent to NO and continue with dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.delayinterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.ignoreEvent = NO; }); } // call sendAction [self swizzl_sendAction:action to:target forEvent:event]; }Copy the code

The overall implementation process, and the implementation of the requirements of the preface.

Prevents arrays from crashing out of bounds

The objectAtIndex method of the swap array checks for overbounds to prevent crashes.

+ (void)load {

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        Class class = NSClassFromString(@"__NSArrayM");

        SEL originalSEL = @selector(objectAtIndex:);

        SEL swizzledSEL = @selector(swizz_objectAtIndex:);

        [JJRuntimeTool jj_MethodSwizzlingWithClass:class oriSEL:originalSEL swizzledSEL:swizzledSEL];

    });

}

- (id)swizz_objectAtIndex:(NSUInteger)index
{
    if (index < self.count) {
        return [self swizz_objectAtIndex:index];
    }else {
        @try {
            return [self swizz_objectAtIndex:index];
        } @catch (NSException *exception) {
            NSLog(@"------- %s Crash Bacause Method %s --------- \n", class_getName(self.class), __func__ );
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        } @finally {
        }
    }
}
Copy the code

Message forwarding mechanism interception crashes

ResolveInstanceMethod if the IMP implementation method is not found after calling objc_msgSend, then the message forwarding mechanism resolveInstanceMethod can dynamically add a method to point to the IMP of our dynamic implementation to prevent the crash.

void testFun(){ NSLog(@"test Fun"); } +(BOOL)resolveInstanceMethod:(SEL)sel{ if ([super resolveInstanceMethod:sel]) { return YES; }else{ class_addMethod(self, sel, (IMP)testFun, "v@:"); return YES; }}Copy the code

Aspects

Section-oriented programming Aspects, without modifying the original function, can insert some code before and after the execution of the function. This is the library I used in my old project in the company. I find it interesting and write it down.

The core is the method aspect_hookSelector.

Selector: a method that requires a hook. Options: an enumeration that defines the timing of the cut (before, after, replace) block: a code block that needs to be executed before and after a selector. Error: Error message */ + (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; /** scope: */ - (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block  error:(NSError **)error;Copy the code

AspectOptions

AspectOptions is an enumeration that defines the timing of the cut, that is, before and after the original method is called, replace the original method, and execute only once (delete the cut logic after the call).

Typedef NS_OPTIONS(NSUInteger, AspectOptions) {AspectPositionAfter = 0, // AspectPositionInstead = 1, AspectPositionBefore = 2 / / / replace the original method, / / / the original method call former executive AspectOptionAutomaticRemoval = 1 < < 3 / / / perform restore section after the operation, namely to cancel hook};Copy the code

AspectsDemo

We want to do something after the current controller calls viewWillAppear.

- (void)viewDidLoad { [super viewDidLoad]; [self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^{ NSLog(@"CCCCCC");  } error:nil]; }Copy the code
- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"AAAAAA");
    [super viewWillAppear:animated];
    NSLog(@"BBBBBB");
}
Copy the code

Running results:

022-01-14 15:32:49.962692+0800 AspectsDemo[68165:356953] AAAAAA

2022-01-14 15:32:49.962785+0800 AspectsDemo[68165:356953] BBBBBB

2022-01-14 15:32:49.962847+0800 AspectsDemo[68165:356953] CCCCCC
Copy the code

Attach gitHub address :Aspects

The resources

  1. IOS Runtime is introduced in detail and used in combat
  2. Runtime implements iOS dictionary model
  3. Deep Parsing of Aspects -iOS section-oriented programming
  4. IOS development: “Runtime” details (ii) Method Swizzling
  5. IOS Development · Runtime Principles and Practices: Methodology Interchange
  6. Introduction to iOS swizzle