ios developers

runtime的使用实例

2019-10-17  本文已影响0人  越来越胖了

前面扯过原理,这里来几个实际用例:

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. 在没有进行动态解析和消息接收者重定向的前提下,我们还可以消息重定向

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.动态操作私有属性

在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;

未完待续...

上一篇下一篇

猜你喜欢

热点阅读