runtime的使用实例
前面扯过原理,这里来几个实际用例:
1.动态方法交换
获取类方法的Mthod
Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
获取实例对象方法的Mthod
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
交换两个方法的实现
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
Runtime动态方法交换更多的是应用于系统类库和第三方框架的方法替换。在不可见源码的情况下,
我们可以借助Rutime交换方法实现,为原有方法添加额外功能,这在实际开发中具有十分重要的意义。
#import <objc/runtime.h>
Method methodA = class_getInstanceMethod([self class], @selector(printA));
Method methodB = class_getInstanceMethod([self class], @selector(printB));
method_exchangeImplementations(methodA, methodB);
[self printA];
2.拦截系统方法
原理还是方法交换,🌰:拦截系统UIFont的systemFontOfSize方法
创建一个UIFont的分类,不需要导入头文件,因为+(void)load方法会自动调用,代码如下:
//.h🔥
@interface UIFont (Adapt)
+ (UIFont *)zs_systemFontOfSize:(CGFloat)fontSize;
@end
//.m🔥
#import "UIFont+Adapt.h"
#import <objc/message.h>
#import <objc/runtime.h>
@implementation UIFont (Adapt)
//用以替换的方法实现
+ (UIFont *)zs_systemFontOfSize:(CGFloat)fontSize{
//获取设备屏幕宽度,并计算出比例scale
CGFloat width = [[UIScreen mainScreen] bounds].size.width;
CGFloat scale = width/375.0 * 2;
//注意:由于方法交换,系统的方法名已变成了自定义的方法名,所以这里使用了
//自定义的方法名来获取UIFont
return [UIFont zs_systemFontOfSize:fontSize * scale];
}
//load方法不需要手动调用,iOS会在应用程序启动的时候自动调起load方法,而且执行时间较早,所以在此方法中执行交换操作比较合适。
+ (void)load {
Method systemMethod = class_getClassMethod([UIFont class], @selector(systemFontOfSize:));
Method selfMethod = class_getClassMethod([self class], @selector(zs_systemFontOfSize:));
method_exchangeImplementations(systemMethod, selfMethod);
}
现在任意VC中调用,都会把字体大小改为width/375.0 * 2 * fontSize
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 30)];
label.text = @"测试Runtime拦截方法";
label.font = [UIFont systemFontOfSize:20];
[self.view addSubview:label];
3.消息动态解析(类的,对象的,两个方法可以使用)
主要用到的函数如下:
OC方法:
类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel
实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
Runtime方法:
/**
运行时方法:向指定类中添加特定方法实现的操作
@param cls 被添加方法的类
@param name selector方法名
@param imp 指向实现方法的函数指针
@param types imp函数实现的返回值与参数类型
@return 添加方法是否成功
*/
BOOL class_addMethod(Class _Nullable cls,
SEL _Nonnull name,
IMP _Nonnull imp,
const char * _Nullable types)
例子:
Person类中创建两个方法:
//声明类方法,但未实现
+ (void)haveMeal:(NSString *)food;
//声明实例方法,但未实现
- (void)singSong:(NSString *)name;
.m中🔥
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
//重写父类方法:处理类方法
+ (BOOL)resolveClassMethod:(SEL)sel{
if(sel == @selector(haveMeal:)){
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(zs_haveMeal:)), "v@");
return YES; //仍然希望正常的消息转发机制进行,只需要返回NO就可以了,否则消息转发流程不执行
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
//重写父类方法:处理实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if(sel == @selector(singSong:)){
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(zs_singSong:)), "v@");
return YES;//仍然希望正常的消息转发机制进行,只需要返回NO就可以了,否则消息转发流程不执行
}
return [super resolveInstanceMethod:sel];
}
+ (void)zs_haveMeal:(NSString *)food{
NSLog(@"%s------%@",__func__,food);
}
- (void)zs_singSong:(NSString *)name{
NSLog(@"%s-----%@",__func__,name);
}
@end
调用没有实现的方法🔥
Person *re_per = [[Person alloc] init];
[re_per singSong:@"唱个锤子哟,方法都没得实现"];
[Person haveMeal:@"代码没撸完,哪有脸吃🍚"];
4.消息接收者重定向(也是两个,类的,对象的)
主要方法就两个:
重定向类方法的消息接收者,返回一个类
- (id)forwardingTargetForSelector:(SEL)aSelector
重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector
例子
在VC中执行以下代码
#import <objc/runtime.h>
#import "Student.h"
@interface ViewController ()
@property(nonatomic,strong)Student *student;
@end
- (void)viewDidLoad {
[super viewDidLoad];
🔥先实例化一个student ,因为Student类中实现了takeExam 和 learnKnowledge 方法
self.student = [[Student alloc] init];
//去执行一个VC中并未声明和实现的 类方法
[ViewController performSelector:@selector(takeExam:) withObject:@"OC"];
//去执行一个VC中并未声明和实现的 实例方法
[self performSelector:@selector(learnKnowledge:) withObject:@"OC底层runtime知识"];
}
🔥 下面的两个方法把 前面 takeExam 和 learnKnowledge 的消息接受者改成了 Student 🔥
//重定向类方法:返回一个类对象
+ (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(takeExam:)) {
return [Student class];
}
return [super forwardingTargetForSelector:aSelector];
}
//重定向实例方法:返回类的实例
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(learnKnowledge:)) {
return self.student;
}
return [super forwardingTargetForSelector:aSelector];
}
这个是student类的实现
@interface Student : NSObject
//类方法:参加考试
+ (void)takeExam:(NSString *)exam;
//实例方法:学习知识
- (void)learnKnowledge:(NSString *)course;
@end
//正常的实现
+ (void)takeExam:(NSString *)exam{
NSLog(@"%s----%@",__func__,exam);
}
- (void)learnKnowledge:(NSString *)course{
NSLog(@"%s----%@",__func__,course);
}
上面就是把消息的接受者改成Student类活实例对象的过程了
5. 在没有进行动态解析和消息接收者重定向的前提下,我们还可以消息重定向
- 当
行动态解析
和消息接收者重定向
方法无法生效,那么这个对象
会因为找不到相应的方法实现而无法响应消息,此时Runtime
系统会通过forwardInvocation:
消息通知该对象,给予此次消息发送; - 最后一次寻找
IMP
的机会:- (void)forwardInvocation:(NSInvocation *)anInvocation;
其实每个对象都从NSObject
类中继承了forwardInvocation:
方法,但是NSObject
中的这个方法只是简单的调用了doesNotRecongnizeSelector:
方法,提示我们错误。所以我们可以重写这个方法:对不能处理的消息做一些默认处理,也可以将消息转发给其他对象来处理而不抛出错误。 - 我们注意到
anInvocation
是forwardInvocation
唯一参数,它封装了原始的消息和消息参数。正是因为它,我们还不得不重写另一个函数:methodSignatureForSelector
。这是因为在
forwardInvocation:
消息发送前,Runtime
系统会向对象发送methodSignatureForSelector
消息,并取到返回的方法签名用于生成NSInvocation
对象。
实例:
VC中
- (void)viewDidLoad {
[super viewDidLoad];
self.student = [[Student alloc] init];
[self performSelector:@selector(learnKnowledge:) withObject:@"天文学"];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"1 2 都没有实现的情况下才会走这个方法:forwardInvocation ");
//1.从anInvocation中获取消息
SEL sel = anInvocation.selector;
//2.判断Student方法是否可以响应应sel
if ([self.student respondsToSelector:sel]) {
//2.1若可以响应,则将消息转发给其他对象处理
[anInvocation invokeWithTarget:self.student];
}else{
//2.2若仍然无法响应,则报错:找不到响应方法
[self doesNotRecognizeSelector:sel];
//(官方:如果覆盖此方法,则必须在实现结束时调用super或引发invalidArgumentException异常。
//换句话说,这个方法不能正常返回;它必须总是导致抛出异常。),所以,不建议重写doesNotRecognizeSelector方法
//其实真的要覆盖也可以,并且可以去实现我们自己的方法如下:(runMethod是我自己随意写的一个方法而已;)
//anInvocation.selector = @selector(runMethod);
//[anInvocation invoke];
}
}
//需要从这个方法中获取的信息来创建NSInvocation对象,因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
return methodSignature;
}
6.获取类的成员变量,属性列表,方法,协议
获取属性列表🔥
unsigned int count;//unsigned-----无符号
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
}
free(propertyList);//释放指针,防止内存泄漏
获取所有成员变量🔥
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i<count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
获取所有方法🔥
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
Method method = methodList[i];
SEL mthodName = method_getName(method);
NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName));
}
free(methodList);
当前遵循的所有协议🔥
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i=0; i<count; i++) {
Protocol *protocal = protocolList[i];
const char *protocolName = protocol_getName(protocal);
NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
}
free(propertyList);
获取成员变量
和方法列表
这两个方法其实很多API中都有用到,比如字典转model,算是使用比较多的;
7.动态操作私有属性
-
情景
:现在假设这样一个情况:我们使用第三方框架里的Person类,在特殊需求下想要更改其私有属性nickName,这样的操作我们就可以使用Runtime可以动态修改对象属性。 -
基本思路
:首先使用Runtime获取Peson对象的所有属性,找到nickName然后使用ivar的方法修改其值。具体的代码示例如下:
在VC中导入person类,添加方法:
-(void)changeIvar{
if(/* DISABLES CODE */ (0)){
//runtime
Person *ps = [[Person alloc] init];
NSLog(@"ps-nickName: %@",[ps valueForKey:@"nickName"]); //呵呵哒
// ps.nickName这个是没有的
🔥第一步:遍历对象的所有属性
unsigned int count;
🔥count为返回数组的长度。如果Count为NULL,则不返回长度.
Ivar *ivarList = class_copyIvarList([ps class], &count);
🔥传入地址&count,则函数内部可以就改count属性
for (int i= 0; i<count; i++) {
🔥第二步:获取每个属性名
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSString *propertyName = [NSString stringWithUTF8String:ivarName];
if ([propertyName isEqualToString:@"_nickName"]) {
🔥第三步:匹配到对应的属性,然后修改;注意属性带有下划线
object_setIvar(ps, ivar, @"越来越胖了😑");
}
}
NSLog(@"ps-nickName: %@",[ps valueForKey:@"nickName"]); //越来越胖了😑
}else{
🔥其实直接使用KVC也可以做到这个  ̄□ ̄||
Person *ps = [[Person alloc] init];
NSLog(@"=======%@", [ps valueForKey:@"nickName"]);
[ps setValue:@"越来越胖了😂" forKey:@"nickName"];
NSLog(@"=======%@", [ps valueForKey:@"nickName"]);
}
}
8.归档解归档(和7一样都是获取属性的使用)
🔥创建person类,代码如下:
//NSCoding协议别忘了
@interface Person : NSObject<NSCoding>
@property(nonatomic,copy)NSString *sex;
@property(nonatomic,copy)NSString *name;
@property(nonatomic,copy)NSString *age;
.m🔥
//解档操作
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
self = [super init];
if (self) {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
const char *ivarName = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:ivarName];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivarList); //释放指针
}
return self;
}
//归档操作
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivarList); //释放指针
}
在VC中调用
#pragma mark - 归档,解归档
-(void)encodeAndDecode{
NSLog(@"归档");
Person * person = [Person new];
person.sex = @"男";
person.name = @"张三";
person.age = @"18";
NSString * temp = NSTemporaryDirectory();
NSString * filePath = [temp stringByAppendingPathComponent:@"person.plist"];
NSLog(@"存储地址-------%@", filePath);
[NSKeyedArchiver archiveRootObject:person toFile:filePath];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"解档");
NSString * temp = NSTemporaryDirectory();
NSString * filePath = [temp stringByAppendingPathComponent:@"person.plist"];
Person *person = (Person *)[NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(@"%@ ---- %@ ---%@", person.name, person.age,person.sex);
}
利用runtime的优势在于不需要去管person有多少属性,代码通用;
9.runtime实现多继承
其实本质上来说,只有C++有多继承的说法,我们只是通过runtime,模拟出多继承而已;
情景:多个VC继承了RootVC,在RootVC中处理了nav,现在某些VC继承了RootVC,但是RootVC内的方法并不满足当前的需求;我们可以在VC中重新实现,这个没有问题,但是如果量很大,又或者下次再变需求,是不是又得把不一样的VC又全部重写一次实现呢? 要是不一样的VC能够再继承一个RootVC就好了,我们就可以再新的RootVC中直接一次性改掉,然而OC并没有多继承0.0;
解决方法:给RootVC写一个分类,重定向需要更改的方法,然后需要修改的VC导入分类,同时在+(void)load中调用方法重定向的实现;
目前就总结了这些,后面还有,后期再补充,包括逆向Hook等,也是使用的runtime;
未完待续...