iOS-runtime简单应用

531 阅读3分钟

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

前言

相信大家在正常开发中, 也会有一些runtime的应用, 好比我原先的那一篇文章<iOS快速归结档> 就是应用了runtime动态获取类中属性名来实现的, 当然水平还是有些次了, runtime博大精深我连个敲门砖估计都算不上, 这里只能列举一些平时常用的东西, 可能有部分同学还是不特别了解的, 可以当个笔记来了解一下就好了

讲解

常用场景

动态获取成员变量

动态获取成员变量 这种方式最具有标志性的功能就是 字典转模型 了, 相信大家项目中这个也是必备选项了, 所以runtime并没有说那么神秘, 毕竟你天天都在用它, 简单来说就是动态获取类中的成员变量, 然后从字典中获取对应名称的数据, 赋值给同名的成员变量. 大概就是这个样子

#import "NSObject+Ex.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (Ex)

+ (instancetype)_initWithDictionaryForModel:(NSDictionary *)dic {
    id myObj = [[self alloc] init];
    unsigned int outCount;
    //获取类中的所有成员属性
    objc_property_t *arrPropertys = class_copyPropertyList([self class], &outCount);
    
    for (NSInteger i = 0; i < outCount; i ++) {
        //获取属性名字符串
        objc_property_t property = arrPropertys[i];
        //model中的属性名
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
        id propertyValue = dic[propertyName];
        
        // 处理服务器字段 与 本地模型 字段不匹配问题
        if ([propertyName isEqualToString:@"age3"]) {
            propertyValue = dic[@"age"];
        }
    
        if (propertyValue != nil) {
            [myObj setValue:propertyValue forKey:propertyName];
        }
    }
    
    //注意在runtime获取属性的时候,并不是ARC Objective-C的对象所有需要释放
    free(arrPropertys);
    return myObj;
}
@end

动态修改成员变量

动态需改成员变量 这种方式最常用到 获取一些私有成员变量, 因为这些变量没有公布出来, 并不能直接修改数据, 通过runtime就可以动态获取到这些变量, 那么就可修改其中的数据, 好比系统的UITextField中, 我们常会修改占位文字的颜色, 而这个属性又是私有的

#import "UITextField+Ex.h"
#import <objc/runtime.h>

@implementation UITextField (Ex)
- (void)changePlaceHolderTextColor:(UIColor *)color {
    //这块就是为了打印出所有的成员变量,获取成员变量的名
    unsigned int count;
    Ivar *ivars = class_copyIvarList([UITextField class], &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        // NSLog(@"成员变量%s", ivar_getName(ivar));
    }
    free(ivars);
     
    // iOS 13 通过 KVC 方式修改私有属性,有 Crash 风险,谨慎使用!并不是所有KVC都会Crash,要尝试!
    if ([[UIDevice currentDevice].systemVersion floatValue] > 13.0) {
        Ivar ivar =  class_getInstanceVariable([UITextField class], "_placeholderLabel");
        UILabel *placeholderLabel = object_getIvar(self, ivar);
        placeholderLabel.textColor = color;
    }else{
        //kvc赋值,iOS13之前用kvc赋值就可以哦
        [self setValue:color forKeyPath:@"_placeholderLabel.textColor"];
  }
}
@end

给分类添加属性

给分类添加属性 这种方式的使用场景是, 给一些系统的类增加功能, 比如 我需要再点击这个按钮的时候, 想给你这个按钮增加一个消息的数据, 但是系统的按钮是不支持的, 自己写一个按钮又比较麻烦, 那么就可以动态的给按钮上增加一个属性来绑定这个消息数据

//.h文件
#import <Foundation/Foundation.h>
@interface NSObject (Ex)
@property (nonatomic, copy) NSString *msg;
@end


//.m文件
import "NSObject+Ex.h"
#import <objc/runtime.h>

@implementation NSObject (Ex)

- (void)setMsg:(id)msg {
    //注意最后一个参数的类型
    objc_setMsg(self, @selector(msg), associatedObject, OBJC_ASSOCIATION_COPY);
}

- (id)msg {
    return objc_getMsg(self, _cmd);
}
@end

方法交换

方法交换 这种方式最常见的就是 无侵入埋点, 相信在很多的埋点文章中或多或少的都会有一部分runtime的影子, 还有一种就是错误处理, 比如 你要给数组中加入一个nill数据, 正常情况肯定是会崩溃的, 但是在加入数据之前, 我们能做一次校验, 如果不是nil的数据才能给加入, 否则就不加, 就会避免用户的体验

#import "NSMutableArray+Ex.h"
#import <objc/runtime.h>

//方法交换
@implementation NSMutableArray (Ex)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 类簇:NSString、NSArray、NSDictionary,真实类型是其他类型
        Class cls = NSClassFromString(@"__NSArrayM");
        Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(cls, @selector(jf_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
    });
}

- (void)jf_insertObject:(id)anObject atIndex:(NSUInteger)index {
    //如果是空对象,就不添加,防止奔溃
    if (anObject == nil) return;
    
    //拦截之后,再调用系统的实现,由于不知道底层实现逻辑,自己实现系统方法能会出各种意想不到的错
    //这儿看似死循环,细理逻辑,方法实现已经交换过了的
    [self jf_insertObject:anObject atIndex:index];
}

@end

总结

上述对runtime的陈述仅仅是冰山一脚, 有非常的多的大神做出了非常厉害的东西, 如果你对我写的东西有什么疑问或者指教, 请告知我 谢谢