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
- IOS Runtime is introduced in detail and used in combat
- Runtime implements iOS dictionary model
- Deep Parsing of Aspects -iOS section-oriented programming
- IOS development: “Runtime” details (ii) Method Swizzling
- IOS Development · Runtime Principles and Practices: Methodology Interchange
- Introduction to iOS swizzle