美文网首页ios developers
runtime的使用实例

runtime的使用实例

作者: 越来越胖了 | 来源:发表于2019-10-17 19:25 被阅读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. 在没有进行动态解析和消息接收者重定向的前提下,我们还可以消息重定向

    • 行动态解析消息接收者重定向方法无法生效,那么这个对象会因为找不到相应的方法实现而无法响应消息,此时Runtime系统会通过forwardInvocation:消息通知该对象,给予此次消息发送;
    • 最后一次寻找IMP的机会:- (void)forwardInvocation:(NSInvocation *)anInvocation;
      其实每个对象都从NSObject类中继承了forwardInvocation:方法,但是NSObject中的这个方法只是简单的调用了doesNotRecongnizeSelector:方法,提示我们错误。所以我们可以重写这个方法:对不能处理的消息做一些默认处理,也可以将消息转发给其他对象来处理而不抛出错误。
    • 我们注意到anInvocationforwardInvocation唯一参数,它封装了原始的消息和消息参数。正是因为它,我们还不得不重写另一个函数: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;

    未完待续...

    相关文章

      网友评论

        本文标题:runtime的使用实例

        本文链接:https://www.haomeiwen.com/subject/ziuvmctx.html