内容简介:最近对一些ios的apm系统比较感兴趣,所以就研究了一些相关的技术。首先从最基本的代码地址-github:
最近对一些ios的apm系统比较感兴趣,所以就研究了一些相关的技术。首先从最基本的 Method Swizzling
开始。
Method Swizzling
是OC runtime提供的一种动态替换方法实现的技术,我们利用它可以替换系统或者我们自定义类的方法实现,进而达到我们的特殊目的。
代码地址-github: MethodSwizzling
Method Swizzling原理
为什么 Method Swizzling
能替换一个类的方法呢?我们首先要理解一下其替换的原理。
OC中的方法在 runtime.h
中的定义如下:
struct objc_method{ SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE; } 复制代码
- method_name: 方法名
- method_types: 方法类型,主要存储着方法的参数类型和返回值类型
- method_imp: 方法的实现,函数指针
由此,我们也可以发现 OC 中的方法名是不包括参数类型的,也就是说下面两个方法在 runtime 看来就是同一个方法:
- (void)viewWillAppear:(BOOL)animated; - (void)viewWillAppear:(NSString *)string; 复制代码
原则上,方法的名称 method_name
和方法的实现 method_imp
是一一对应的,而 Method Swizzling 的原理就是动态地改变它们的对应关系,以达到替换方法实现的目的。
Method Swizzling
应用
runtime中和方法替换相关的函数
class_getInstanceMethod
OBJC_EXPORT Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name); 复制代码
- 作用:获取一个类的实例方法
- cls : 方法所在的类
- name: 选择子的名称(选择子就是方法名称)
class_getClassMethod
OBJC_EXPORT Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name); 复制代码
- 作用:获取一个类的类方法
- cls : 方法所在的类
- name: 选择子名称
method_getImplementation
OBJC_EXPORT IMP _Nonnull method_getImplementation(Method _Nonnull m); 复制代码
- 作用: 根据方法获取方法的指针
- m : 方法
method_getTypeEncoding
OBJC_EXPORT const char * _Nullable method_getTypeEncoding(Method _Nonnull m); 复制代码
- 作用: 获取方法的参数和返回值类型描述
- m : 方法
class_addMethod
OBJC_EXPORT BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types); 复制代码
- 作用: 给类添加一个新方法和该方法的实现
- 返回值: yes,表示添加成功; No,表示添加失败
- cls: 将要添加方法的类
- name: 将要添加的方法名
- imp: 实现这个方法的指针
- types: 要添加的方法的返回值和参数
method_exchangeImplementations
OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2); 复制代码
- 交换两个方法
class_replaceMethod
OBJC_EXPORT IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) ; 复制代码
- 作用: 指定替换方法的实现
- cls : 将要替换方法的类
- name: 将要替换的方法名
- imp: 新方法的指针
- types: 新方法的返回值和参数描述
替换一个类的实例方法
eg: 替换 UIViewController
中的 viewDidLoad
方法。
#import "UIViewController+MI.h" #import <objc/runtime.h> @implementation UIViewController (MI) + (void)load{ // 替换ViewController中的viewDidLoad方法 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL origin_selector = @selector(viewDidLoad); SEL swizzed_selector = @selector(mi_viewDidLoad); Method origin_method = class_getInstanceMethod([self class], origin_selector); Method swizzed_method = class_getInstanceMethod([self class], swizzed_selector); BOOL did_add_method = class_addMethod([self class], origin_selector, method_getImplementation(swizzed_method), method_getTypeEncoding(swizzed_method)); if (did_add_method) { NSLog(@"debugMsg: ViewController类中没有viewDidLoad方法(可能在其父类h中),所以先添加后替换"); class_replaceMethod([self class], swizzed_selector, method_getImplementation(origin_method), method_getTypeEncoding(origin_method)); }else{ NSLog(@"debugMsg: 直接交换方法"); method_exchangeImplementations(origin_method, swizzed_method); } }); } - (void)mi_viewDidLoad { [self mi_viewDidLoad]; NSLog(@"debugMsg: 替换成功"); } 复制代码
对以上代码做简要说明:
+(void)load class_addMethod
替换成功。控制台信息:
2019-04-17 17:25:16.937849+0800 MethodSwizzling[4975:639584] debugMsg: 直接交换方法 2019-04-17 17:25:17.025214+0800 MethodSwizzling[4975:639584] debugMsg: 替换成功 复制代码
替换一个类的实例方法到另一个类
当我们对私有类库(不知道该类的头文件,只知道有这个类并且已知该类中的一个方法),此时我们需要hook这个类的方法到一个新类中。
eg: 我们要hook person类中有一个 speak:
方法方法:
#import "Person.h" @implementation Person - (void)speak:(NSString *)language { NSLog(@"person speak language: %@",language); } + (void)sleep:(NSUInteger)hour { NSLog(@"person sleep: %lu",hour); } @end 复制代码
我们新建 ChinesePerson
,hook speak:
方法到 ChinesePerson
中。
#import "ChinesePerson.h" #import <objc/runtime.h> @implementation ChinesePerson + (void)load { Class origin_class = NSClassFromString(@"Person"); Class swizzed_class = [self class]; SEL origin_selector = NSSelectorFromString(@"speak:"); SEL swizzed_selector = NSSelectorFromString(@"mi_speak:"); Method origin_method = class_getInstanceMethod(origin_class, origin_selector); Method swizzed_method = class_getInstanceMethod(swizzed_class, swizzed_selector); BOOL add_method = class_addMethod(origin_class, swizzed_selector, method_getImplementation(swizzed_method), method_getTypeEncoding(swizzed_method)); if (!add_method) { return; } swizzed_method = class_getInstanceMethod(origin_class, swizzed_selector); if (!swizzed_method) { return; } BOOL did_add_method = class_addMethod(origin_class, origin_selector, method_getImplementation(swizzed_method), method_getTypeEncoding(swizzed_method)); if (did_add_method) { class_replaceMethod(origin_class, swizzed_selector, method_getImplementation(origin_method), method_getTypeEncoding(origin_method)); }else{ method_exchangeImplementations(origin_method, swizzed_method); } } - (void)mi_speak:(NSString *)language { if ([language isEqualToString:@"Chinese"]) { [self mi_speak:language]; } } 复制代码
替换成功。控制台信息(只打印汉语):
2019-04-17 17:25:17.025362+0800 MethodSwizzling[4975:639584] person speak language: Chinese 复制代码
替换类方法
eg: 我们替换person类中的 sleep:
方法:
#import "Person+MI.h" #import <objc/runtime.h> @implementation Person (MI) + (void)load { Class class = [self class]; SEL origin_selector = @selector(sleep:); SEL swizzed_selector = @selector(mi_sleep:); Method origin_method = class_getClassMethod(class, origin_selector); Method swizzed_method = class_getClassMethod(class,swizzed_selector); if (!origin_method || !swizzed_method) { return; } IMP origin_imp = method_getImplementation(origin_method); IMP swizzed_imp = method_getImplementation(swizzed_method); const char* origin_type = method_getTypeEncoding(origin_method); const char* swizzed_type = method_getTypeEncoding(swizzed_method); // 添加方法到MetaClass中 Class meta_class = objc_getMetaClass(class_getName(class)); class_replaceMethod(meta_class, swizzed_selector, origin_imp, origin_type); class_replaceMethod(meta_class, origin_selector, swizzed_imp, swizzed_type); } + (void)mi_sleep:(NSUInteger)hour { if (hour >= 7) { [self mi_sleep:hour]; } } @end 复制代码
控制台打印(睡眠大于等于7小时才打印----呼吁健康睡眠):
2019-04-17 17:25:17.025465+0800 MethodSwizzling[4975:639584] person sleep: 8 复制代码
类方法的hook和实例方法的hook有两点不同:
-
获取Method的方法变更为
class_getClassMethod(Class cls, SEL name)
,不是class_getInstanceMethod(Class cls, SEL name)
; - 对于类方法的动态添加,需要将方法添加到MetaClass中,因为实例方法记录在class的method-list中, 类方法是记录在meta-class中的method-list中的.
替换类簇中的方法
#import "MIMutableDictionary.h" #import <objc/runtime.h> @implementation MIMutableDictionary + (void)load { Class origin_class = NSClassFromString(@"__NSDictionaryM"); Class swizzed_class = [self class]; SEL origin_selector = @selector(setObject:forKey:); SEL swizzed_selector = @selector(mi_setObject:forKey:); Method origin_method = class_getInstanceMethod(origin_class, origin_selector); Method swizzed_method = class_getInstanceMethod(swizzed_class, swizzed_selector); IMP origin_imp = method_getImplementation(origin_method); IMP swizzed_imp = method_getImplementation(swizzed_method); const char* origin_type = method_getTypeEncoding(origin_method); const char* swizzed_type = method_getTypeEncoding(swizzed_method); class_replaceMethod(origin_class, swizzed_selector, origin_imp, origin_type); class_replaceMethod(origin_class, origin_selector, swizzed_imp, swizzed_type); } - (void)mi_setObject:(id)objContent forKey:(id<NSCopying>)keyContent { if (objContent && keyContent) { NSLog(@"执行了进去"); [self mi_setObject:objContent forKey:keyContent]; } } @end 复制代码
应用
不推荐在项目中过多的使用 Method Swizzling
,不然的话原生的类都被hook的非常乱,项目出问题时非常难定位问题。有一篇文章说它是ios中的一个毒瘤。 iOS届的毒瘤-MethodSwizzling
尽管如此,我们还是了解并梳理一下其应用场景来感受一下这种技术。
防止数组取值时越界crash
不仅仅是数组, NSDictionary
的利用runtime防止崩溃是同样的原理。
#import "NSArray+Safe.h" #import <objc/runtime.h> @implementation NSArray (Safe) + (void)load { // objectAtIndex: 方式取元素 Method origin_method = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:)); Method replaced_method = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safeObjectAtIndex:)); method_exchangeImplementations(origin_method, replaced_method); Method origin_method_muta = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:)); Method replaced_method_muta = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(safeMutableObjectAtIndex:)); method_exchangeImplementations(origin_method_muta, replaced_method_muta); // 直接通过数组下标的方式取元素 Method origin_method_sub = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:)); Method replaced_method_sub = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safeObjectAtIndexedSubscript:)); method_exchangeImplementations(origin_method_sub, replaced_method_sub); Method origin_method_muta_sub = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndexedSubscript:)); Method replaced_method_muta_sub = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(safeMutableObjectAtIndexedSubscript:)); method_exchangeImplementations(origin_method_muta_sub, replaced_method_muta_sub); } - (id)safeObjectAtIndex:(NSUInteger)index { if (self.count > index && self.count) { return [self safeObjectAtIndex:index]; } NSLog(@"errorMsg:数组[NSArray]越界..."); return nil; } - (id)safeMutableObjectAtIndex:(NSUInteger)index { if (self.count > index && self.count) { return [self safeMutableObjectAtIndex:index]; } NSLog(@"errorMsg:数组[NSMutableArray]越界..."); return nil; } -(id)safeObjectAtIndexedSubscript:(NSUInteger)index { if (self.count > index && self.count) { return [self safeObjectAtIndexedSubscript:index]; } NSLog(@"errorMsg:数组[NSArray]越界..."); return nil; } - (id)safeMutableObjectAtIndexedSubscript:(NSUInteger)index { if (self.count > index && self.count) { return [self safeMutableObjectAtIndexedSubscript:index]; } NSLog(@"errorMsg:数组[NSMutableArray]越界..."); return nil; } @end 复制代码
使用:
- (void)test2 { NSArray *arr = @[@"a",@"b",@"c",@"d",@"e",@"f"]; NSLog(@"atIndex方式: %@",[arr objectAtIndex:10]); NSLog(@"下标方式: %@",arr[10]); } 复制代码
控制台输出log:
2019-04-18 19:14:18.139417+0800 MethodSwizzling[25379:1703659] errorMsg:数组[NSArray]越界... 2019-04-18 19:14:18.139536+0800 MethodSwizzling[25379:1703659] atIndex方式: (null) 2019-04-18 19:14:18.139793+0800 MethodSwizzling[25379:1703659] errorMsg:数组[NSArray]越界... 2019-04-18 19:14:18.139868+0800 MethodSwizzling[25379:1703659] 下标方式: (null) 复制代码
改变app中所有按钮的大小
常规做法是遍历视图中的所有子视图,把所有的button整体改变。 此时我们使用runtime改变按钮的大小。
#import "UIButton+Size.h" #import <objc/runtime.h> @implementation UIButton (Size) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 增大所有按钮大小 Method origin_method = class_getInstanceMethod([self class], @selector(setFrame:)); Method replaced_method = class_getInstanceMethod([self class], @selector(miSetFrame:)); method_exchangeImplementations(origin_method, replaced_method); }); } - (void)miSetFrame:(CGRect)frame { frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width+20, frame.size.height+20); NSLog(@"设置按钮大小生效"); [self miSetFrame:frame]; } @end 复制代码
处理按钮重复点击
如果重复过快的点击同一个按钮,那么就会多次触发和按钮绑定的事件。处理这种case的方式有很多种,通过 Method Swillzing
也能解决这种问题。
.h文件:
#import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface UIButton (QuickClick) @property (nonatomic,assign) NSTimeInterval delayTime; @end NS_ASSUME_NONNULL_END 复制代码
#import "UIButton+QuickClick.h" #import <objc/runtime.h> @implementation UIButton (QuickClick) static const char* delayTime_str = "delayTime_str"; + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method originMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:)); Method replacedMethod = class_getInstanceMethod(self, @selector(miSendAction:to:forEvent:)); method_exchangeImplementations(originMethod, replacedMethod); }); } - (void)miSendAction:(nonnull SEL)action to:(id)target forEvent:(UIEvent *)event { if (self.delayTime > 0) { if (self.userInteractionEnabled) { [self miSendAction:action to:target forEvent:event]; } self.userInteractionEnabled = NO; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.delayTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.userInteractionEnabled = YES; }); }else{ [self miSendAction:action to:target forEvent:event]; } } - (NSTimeInterval)delayTime { return [objc_getAssociatedObject(self, delayTime_str) doubleValue]; } - (void)setDelayTime:(NSTimeInterval)delayTime { objc_setAssociatedObject(self, delayTime_str, @(delayTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end 复制代码
以上所述就是小编给大家介绍的《ios runtime之Method Swizzling及其应用场景》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
小米生态链战地笔记
小米生态链谷仓学院 / 中信出版集团 / 2017-5 / 56.00
2013年下半年,小米开始做一件事,就是打造一个生态链布局IoT(物联网);2016年年底,小米生态链上已经拥有了77家企业,生态链企业整体销售额突破100亿元。这3年,是小米生态链快速奔跑的3年,也是小米在商场中不断厮杀着成长的3年。 3年,77家生态链企业,16家年销售额破亿,4家独角兽公司,边实战,边积累经验。 小米生态链是一个基于企业生态的智能硬件孵化器。过去的3年中,在毫无先......一起来看看 《小米生态链战地笔记》 这本书的介绍吧!