The Runtime collection

Ios-runtime basics

1. What is Method Swizzling

It’s usually called Method swapping or Method spoofing. Method Swizzling is used to change an existing implementation of a selector, by changing the mapping between the selector and the class’s methodLists while the program is running. It’s essentially an IMP that swaps the two methods.

Methods correspond to the objc_Method structure, which contains SEL method_name (Method name) and IMP (Method implementation)

struct objc_method {
    SEL _Nonnull method_name;
    char * _Nullable method_types;
    IMP _Nonnull method_imp;
};
Copy the code

Method (Method),SEL (method name),IMP (Method implementation)The relationship can be expressed this way: At run time,Class (Class)Maintained aMethod list) to determine the correct delivery of the message.Method listThe element that we store isMethod (Method). whileMethod (Method)Is mapped to a pair of key-value pairs:SEL (method name) : IMP (method implementation).Method SwizzlingTo modify theMethod listMake a differenceMethod (Method)Key – value pairs have been swapped in. Let’s say I swap the first two key-value pairsSEL A :IMP A,SEL B :IMP BAnd then when you swap it, it becomes thetaSEL A :IMP B,SEL B: IMP AAs shown in the figure:

2 Method Swizzling bottom source code

(the Runtime source code download) [opensource.apple.com/tarballs/ob…]. The source code is in objc-class-old.m

void method_exchangeImplementations(Method m1_gen, Method m2_gen) { IMP m1_imp; old_method *m1 = oldmethod(m1_gen); old_method *m2 = oldmethod(m2_gen); if (! m1 || ! m2) return; impLock.lock(); m1_imp = m1->method_imp; m1->method_imp = m2->method_imp; m2->method_imp = m1_imp; impLock.unlock(); }Copy the code

Or objc runtime – new. M

void method_exchangeImplementations(Method m1, Method m2) { if (! m1 || ! m2) return; mutex_locker_t lock(runtimeLock); IMP imp1 = m1->imp(false); IMP imp2 = m2->imp(false); SEL sel1 = m1->name(); SEL sel2 = m2->name(); m1->setImp(imp2); m2->setImp(imp1); // RR/AWZ updates are slow because class is unknown // Cache updates are slow because class is unknown // fixme build list of classes whose Methods are known externally? flushCaches(nil, __func__, [sel1, sel2, imp1, imp2](Class c){ return c->cache.shouldFlush(sel1, imp1) || c->cache.shouldFlush(sel2, imp2); }); adjustCustomFlagsForMethodChange(nil, m1); adjustCustomFlagsForMethodChange(nil, m2); }Copy the code

2. Method Swizzling

+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector { Class msclass = cls; Method originalMethod = class_getInstanceMethod(msclass, originalSelector); Method swizzledMethod = class_getInstanceMethod(msclass, swizzledSelector); BOOL didAddMethod = class_addMethod(msclass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(msclass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }}Copy the code

3. Precautions for using Method Swizzling

  1. Method Swizzling should only be performed in +load

When the program starts, all classes are loaded first, and the +load method for each class is called. And it will only be called once during the entire program lifecycle (excluding external display calls). So Method Swizzling in the +load Method is the best way to do it.

Why not use the +initialize method instead?

The +initialize method is called only when the first message is sent to the class. If the class is only referenced and not called, the +initialize method is not executed. Method Swizzling affects the global state, and the +load Method ensures that the exchange occurs when the class is loaded, ensuring that the exchange results. Using the +initialize method does not guarantee this and may not function as an exchange method when used.

  1. Do not call [super load] when Method Swizzling is executed in +load; .

As we said, when the program starts, it loads all the classes first. If you call the [super Load] Method from the + (void)load Method, the superclass’s Method Swizzling will be executed twice, and the Method swap will be performed twice. This invalidates the Method Swizzling of the parent class.

  1. Method Swizzling should always be executed in dispatch_once

Method Swizzling is not an atomic operation and dispatch_once ensures that code is executed only once, even in different threads. Therefore, we should always perform Method Swizzling on dispatch_once to ensure that Method substitution is performed only once.

4. Method Swizzling application scenario

Method Swizzling can swap the implementation of the two methods, and is more applied to the system class library in development, as well as the Method replacement of third-party frameworks. In cases where the official source code is not available, we can use the Method Swizzling Runtime to add additional functionality to the existing methods

4.1 Global Page Statistics function

@implementation UIViewController (HIAnalysis) + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(analysis_viewDidLoad); [HIHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector]; originalSelector = @selector(viewWillAppear:); swizzledSelector = @selector(analysis_viewWillAppear:); [HIHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector]; originalSelector = @selector(viewDidAppear:); swizzledSelector = @selector(analysis_viewDidAppear:); [HIHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector]; originalSelector = @selector(viewWillDisappear:); swizzledSelector = @selector(analysis_viewWillDisappear:); [HIHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector]; originalSelector = @selector(viewDidDisappear:); swizzledSelector = @selector(analysis_viewDidDisappear:); [HIHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector]; }); } - (void)analysis_viewDidLoad{ [self analysis_viewDidLoad]; } - (void)analysis_viewWillAppear:(BOOL)animated{ void(^fetchTitleHandler)(void) = ^(){ NSString *title = self.title; if (! isStrEmpty(title) && ! [self isKindOfClass:[UINavigationController class]] && ! [self isKindOfClass:[UITabBarController class]]) { [HIAppAnalytics HI_trackPageBegin:title]; }}; if ([self respondsToSelector:@selector(analysisName)]) { NSString *analysisName = [self performSelector:@selector(analysisName)]; if (! isStrEmpty(analysisName)) { [HIAppAnalytics HI_trackPageBegin:analysisName]; }else{ fetchTitleHandler(); } }else{ fetchTitleHandler(); } [self analysis_viewWillAppear:animated]; }Copy the code

4. 2 Fonts fit according to screen size

  1. Create a Category for UIFont.
  2. Implement a custom in the classification

The xxx_systemFontOfSize: method in which to add a method to scale the font. 3. Use Method Swizzling to swap the systemFontOfSize: Method with xxx_systemFontOfSize: Method.

#import "UIFont+AdjustSwizzling.h" #import <objc/runtime.h> #define XXX_UISCREEN_WIDTH 375 @implementation UIFont (AdjustSwizzling) + (void)load { static dispatch_once_t onceToken; Dispatch_once (&onceToken, ^{// Class methods are stored in metaclass objects, Class = object_getClass((id)self); SEL originalSelector = @selector(systemFontOfSize:); SEL swizzledSelector = @selector(xxx_systemFontOfSize:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }}); } + (UIFont *)xxx_systemFontOfSize:(CGFloat)fontSize { UIFont *newFont = nil; newFont = [UIFont xxx_systemFontOfSize:fontSize * [UIScreen mainScreen].bounds.size.width / XXX_UISCREEN_WIDTH]; return newFont; } @endCopy the code

4.3 Global Processing Button is repeatedly clicked

  1. Create a Category for UIControl or UIButton.
  2. Add an NSTimeInterval xxx_acceptEventInterval to the class. Property to set the repeat click interval
  3. Implement a custom xxx_sendAction:to:forEvent: method in the classification, adding the corresponding time-bound method.
  4. Swap the sendAction:to:forEvent: Method with xxx_sendAction:to:forEvent: Method Swizzling.
#import "UIButton+ delayswizzling.h "#import <objc/runtime.h> @interface UIButton() assign) NSTimeInterval xxx_acceptEventInterval; // Last time you clicked the timestamp @property (nonatomic, assign) NSTimeInterval xxx_acceptEventTime; @end @implementation UIButton (DelaySwizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(sendAction:to:forEvent:); SEL swizzledSelector = @selector(xxx_sendAction:to:forEvent:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }}); } - (void)xxx_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { If (self.xxx_accepteventInterval <= 0) {// If there is no custom interval, the default is 0.4 seconds self.xxx_accepteventInterval = 0.4; } / / is less than the set time interval BOOL needSendAction = (NSDate. Date. TimeIntervalSince1970 - self. Xxx_acceptEventTime > = self.xxx_acceptEventInterval); / / update the last click the timestamp if (self) xxx_acceptEventInterval > 0) {self. Xxx_acceptEventTime = NSDate. Date. TimeIntervalSince1970; } if (needSendAction) {[self xxx_sendAction: Action to:target forEvent:event]; } } - (NSTimeInterval )xxx_acceptEventInterval{ return [objc_getAssociatedObject(self, "UIControl_acceptEventInterval") doubleValue]; } - (void)setXxx_acceptEventInterval:(NSTimeInterval)xxx_acceptEventInterval{ objc_setAssociatedObject(self, "UIControl_acceptEventInterval", @(xxx_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSTimeInterval )xxx_acceptEventTime{ return [objc_getAssociatedObject(self, "UIControl_acceptEventTime") doubleValue]; } - (void)setXxx_acceptEventTime:(NSTimeInterval)xxx_acceptEventTime{ objc_setAssociatedObject(self, "UIControl_acceptEventTime", @(xxx_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @endCopy the code

4.4 Placeholder map is incorrectly loaded in TableView and CollectionView

Create a Category for TableView and add a refresh callback block property and a placeholder View property to the Category. Implement a custom xxx_reloadData method in the classification, adding code to determine whether it is empty and to load and hide a placeholder map. Use Method Swizzling to exchange methods of reloadData and xxx_reloadData.

#import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface UITableView (ReloadDataSwizzling) @property (nonatomic, assign) BOOL firstReload; @property (nonatomic, strong) UIView *placeholderView; @property (nonatomic, copy) void(^reloadBlock)(void); @end /*--------------------------------------*/ #import "UITableView+ReloadDataSwizzling.h" #import "XXXPlaceholderView.h" #import <objc/runtime.h> @implementation UITableView (ReloadDataSwizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(reloadData); SEL swizzledSelector = @selector(xxx_reloadData); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }}); } - (void)xxx_reloadData { if (! self.firstReload) { [self checkEmpty]; } self.firstReload = NO; [self xxx_reloadData]; } - (void)checkEmpty { BOOL isEmpty = YES; // null flag id <UITableViewDataSource> dataSource = self. NSInteger sections = 1; / / the default TableView only a set of the if ([dataSource respondsToSelector: @ the selector (numberOfSectionsInTableView:)]) {sections = [dataSource numberOfSectionsInTableView:self] - 1; } for (NSInteger I = 0; i <= sections; i++) { NSInteger rows = [dataSource tableView:self numberOfRowsInSection:i]; If (rows) {isEmpty = NO; }} if (isEmpty) {// If empty, load the placeholder map if (! Self. PlaceholderView) {/ / if not custom, load default placeholder figure [self makeDefaultPlaceholderView]; } self.placeholderView.hidden = NO; [self addSubview:self.placeholderView]; } else {/ / is empty, not hide placeholder figure self. PlaceholderView. Hidden = YES; } } - (void)makeDefaultPlaceholderView { self.bounds = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height); XXXPlaceholderView *placeholderView = [[XXXPlaceholderView alloc] initWithFrame:self.bounds]; __weak typeof(self) weakSelf = self; [placeholderView setReloadClickBlock:^{ if (weakSelf.reloadBlock) { weakSelf.reloadBlock(); } }]; self.placeholderView = placeholderView; } - (BOOL)firstReload { return [objc_getAssociatedObject(self, @selector(firstReload)) boolValue]; } - (void)setFirstReload:(BOOL)firstReload { objc_setAssociatedObject(self, @selector(firstReload), @(firstReload), OBJC_ASSOCIATION_ASSIGN); } - (UIView *)placeholderView { return objc_getAssociatedObject(self, @selector(placeholderView)); } - (void)setPlaceholderView:(UIView *)placeholderView { objc_setAssociatedObject(self, @selector(placeholderView), placeholderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void (^)(void))reloadBlock { return objc_getAssociatedObject(self, @selector(reloadBlock)); } - (void)setReloadBlock:(void (^)(void))reloadBlock { objc_setAssociatedObject(self, @selector(reloadBlock), reloadBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } @endCopy the code

4.5 APM (Application Performance Management) to prevent program crashes

Replace NSURLConnection with Method Swizzling, the original implementation of nSURlSession-related (such as the constructor of NSURLConnection and the start Method), and add network performance burying behavior to the implementation. The original implementation is then called. To monitor the network. To prevent a program from crashing, use Method Swizzling to intercept system methods that are prone to crashing, and then use the replacement Method to catch the exception type NSException before handling the exception. The most common example is intercepting the arrayWithObjects: Count: method to keep an array out of bounds

@implementation NSArray (ArrayBounds) + (void)load{ [super load]; static dispatch_once_t onceToken; Dispatch_once (&oncetoken, ^{dispatch_once(&oncetoken, ^{dispatch_once(&oncetoken, ^{ NSString *tmpFirstStr = @"safe_ZeroObjectAtIndex:"; NSString *tmpThreeStr = @"safe_objectAtIndex:"; NSString *tmpSecondStr = @"safe_singleObjectAtIndex:"; / / replace objectAtIndexedSubscript nsstrings * tmpSubscriptStr = @ "objectAtIndexedSubscript:"; NSString *tmpSecondSubscriptStr = @"safe_objectAtIndexedSubscript:"; [HIHookUtility swizzlingInClass:NSClassFromString(@"__NSArray0") originalSelector:NSSelectorFromString(tmpStr) swizzledSelector:NSSelectorFromString(tmpFirstStr)]; [HIHookUtility swizzlingInClass:NSClassFromString(@"__NSSingleObjectArrayI") originalSelector:NSSelectorFromString(tmpStr) swizzledSelector:NSSelectorFromString(tmpSecondStr)]; [HIHookUtility swizzlingInClass:NSClassFromString(@"__NSArrayI") originalSelector:NSSelectorFromString(tmpStr) swizzledSelector:NSSelectorFromString(tmpThreeStr)]; [HIHookUtility swizzlingInClass:NSClassFromString(@"__NSArrayI") originalSelector:NSSelectorFromString(tmpSubscriptStr) swizzledSelector:NSSelectorFromString(tmpSecondSubscriptStr)]; }); } #pragma mark -- implement method /** Retrieve NSArray first index for __nsarrayi@param index index @return return value */ - (id)safe_objectAtIndex:(NSUInteger)index { if (index > self.count - 1 || ! self.count){ @try { return [self safe_objectAtIndex:index]; } @catch (NSException *exception) {//__throwOutException throws an exception NSAssert(NO, @"-- index overthrows!!" ); NSLog(@"exception: %@", exception.reason); return nil; } @finally { } }else{ return [self safe_objectAtIndex:index]; }}Copy the code

Refer to the Runtime for details. (2) Method Swizzling