美文网首页demoiOS开发iOS Developer
iOS 开发之runtime使用小结

iOS 开发之runtime使用小结

作者: 重驹 | 来源:发表于2017-06-02 15:07 被阅读94次

    我们一般用runtime做以下这些事情:

     1.动态交换两个方法的实现(同时也可以替换系统的方法)
     2.获得某个类的所有成员方法、所有成员变量
     3.动态的改变属性值、增加一个属性
     4.动态的增加一个方法
     5.实现NSCoding的自动归档和解档
     6.实现字典转模型的自动转换
    

    一、使用runtime如何交换两个方法的实现,拦截系统自带的方法调用功能。

    1.交换两个方法的实现:

    首先我们创建一个工程,使用市面上最常用的一个例子,创建一个Person类,写两个类方法、两个实例方法。
    .h

    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    + (void)eat;
    + (void)sleep;
    - (void)study;
    - (void)playGame;
    @end
    

    .m

    #import "Person.h"
    
    @implementation Person
    + (void)eat{
        NSLog(@"哥么 吃了");
    }
    + (void)sleep{
        NSLog(@"哥么 睡了");
    }
    - (void)study{
        NSLog(@"小伙儿 在学习");
    }
    - (void)playGame{
        NSLog(@"小伙儿 在打游戏");
    }
    @end
    

    预备东西已经准备完全,现在开始交换两个方法。
    在用runtime交换两个方法的时候,首先需要倒入头文件#import <objc/runtime.h>。之后你需要了解下面三个方法是干什么用的。

    //获得某个类的类方法
    Method class_getClassMethod(Class cls , SEL name)
    //获得某个类的实例对象方法
    Method class_getInstanceMethod(Class cls , SEL name)
    //交换两个方法的实现
    void method_exchangeImplementations(Method m1 , Method m2)
    

    了解了上面的知识点后,就进入我们的主题了:
    案例1:交换两个类方法

    //交换类方法
    Method method1 = class_getClassMethod([Person class], @selector(eat));
    Method method2 = class_getClassMethod([Person class], @selector(sleep));
    method_exchangeImplementations(method1, method2);
    [Person eat];
    [Person sleep];
    

    案例2:交换实例方法

    //交换实例方法
    Method method3 = class_getInstanceMethod([Person class], @selector(study));
    Method method4 = class_getInstanceMethod([Person class], @selector(playGame));
    method_exchangeImplementations(method3, method4);
    Person *person = [[Person alloc] init];
    [person study];
    [person playGame];
    

    2.拦截系统方法:

    例如在iOS7出来之后,设计风格偏向于扁平化 扁平化设计风格介绍 ,有些公司为了适应趋势,会做出图片的大批量更改,如果不想一张一张替换原先的图片,就可以使用runtime截获imageNamed方法,作出相应的处理:
    我们创建一个UIImage的分类,在.m文件里里重写一个类方法

    + (UIImage *)LF_imageNamed:(NSString *)name {
    double version = [[UIDevice currentDevice].systemVersion doubleValue];
    if (version >= 7.0) {
        name = [name stringByAppendingString:@"_change"];
    }
      return [UIImage LF_imageNamed:name];
    }
    

    我们可以在load方法中将重写的方法跟imageNamed方法互换:

    + (void)load {
    // 获取两个类的类方法
    Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
    Method m2 = class_getClassMethod([UIImage class], @selector(LF_imageNamed:));
    // 开始交换方法实现
    method_exchangeImplementations(m1, m2);
    }
    

    上面内容的Demo在这里

    二、使用runtime获得某个类的所有成员方法、所有成员变量

    class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
    

    上面的方法是获取属性需要用的方法

    class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
    

    上面的方法是获取类中所有方法的方法
    首先创建一个继承NSObject的Student类,在.h文件中

    #import <Foundation/Foundation.h>
    @interface Student : NSObject
    @property (assign, nonatomic) int age;
    - (void)study;
    - (void)sleep;
    
    @end
    

    .m

    #import "Student.h"
    @interface Student()
    {
        NSString *name;
    }
    @end
    @implementation Student
    //初始化person属性
    -(instancetype)init{
        self = [super init];
        if(self) {
            name = @"Tom";
            self.age = 12;
        } 
        return self;
    }
    - (void)study{
        NSLog(@"学生要学习");
    }
    - (void)sleep{
        NSLog(@"学生要睡觉");
    }
    //输出person对象时的方法:
    -(NSString *)description{
        return [NSString stringWithFormat:@"name:%@ age:%d",name,self.age];
    }
    @end
    

    注意在.m文件中我们声明了私有实例变量,这个我们也是可以通过runtime获取到的。
    我们在ViewController中进行获取全部属性 和全部方法的实现代码

    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([Student class], &outCount);
    
    // 遍历所有成员变量
    for (int i = 0; i < outCount; i++) {
        // 取出i位置对应的成员变量
        Ivar ivar = ivars[i];
        const char *name = ivar_getName(ivar);
        const char *type = ivar_getTypeEncoding(ivar);
        NSLog(@"成员变量名:%s 成员变量类型:%s",name,type);
    }
    // 注意释放内存!
    free(ivars);
    

    上面是获取所有成员变量的方法,获取的结果如下:

    2017-05-27 14:09:53.786 runtime-归档解档[4081:960384] 成员变量名:name 成员变量类型:@"NSString"
    2017-05-27 14:09:53.787 runtime-归档解档[4081:960384] 成员变量名:_age 成员变量类型:i
    

    其中的i代表的是int类型。
    这里经常在其他博主文章那里会看到有人问" "和没有下划线的问题。这里就不得不提到另一个方法copyPropertyList。copyPropertyList和copyIvarList的区别就在于后者能够获取到"{}"中的成员变量,而前者只能获取到“@property”声明的属性变量。而copyIvarList获取到的一般都会主动在成员变量名称前面加上“”,当然如果是“{}”中的成员变量copyIvarList也不会主动加“”。

    三、动态的改变属性值、增加一个属性

    //改变属性值
    - (IBAction)changeVariable:(UIButton *)sender {
        NSLog(@"打印当前对象 -- %@",student);
        unsigned int count = 0;
        Ivar *variLists = class_copyIvarList([Student class], &count);
        Ivar ivar = variLists[0];//这里我们知道第一个参数是name,所以就直接取第一个元素
        const char *str = ivar_getName(ivar);
        NSLog(@"得到的Ivar是 -- %s",str);
    
        object_setIvar(student, ivar, @"Mars");
        NSLog(@"改变之后的student:%@",student);
    }
    

    怎样获取属性,在第二块已经说过了,这里我们获取到属性之后,使用

    object_setIvar(<#id obj#>, <#Ivar ivar#>, <#id value#>)
    

    这个方法,进行设置属性的值。

    2017-05-27 14:41:24.113 runtime-归档解档[4081:960384] 打印当前对象 -- name:Tom age:12
    2017-05-27 14:41:24.114 runtime-归档解档[4081:960384] 得到的Ivar是 -- name
    2017-05-27 14:41:24.114 runtime-归档解档[4081:960384] 改变之后的student:name:Mars age:12
    

    从输出的log中,我们可以看出,初始值name是Tom,在用object_setIvar这个方法进行了设置属性之后,name变成了Mars,这样就做到动态的修改属性值的效果了。

    动态的添加一个属性,我们往往是创建某个类的分类,之后在这个分类里面做处理,譬如这里,我们给Student类添加一个属性,我们在Student的分类.h里面生命一个属性

    @property (nonatomic,assign)float height; //新属性
    

    在分类的.m文件里面

    #import "Student+category.h"
    #import <objc/runtime.h> 
    
    const char * str = "myKey"; //做为key,字符常量 必须是C语言字符串;
    
    @implementation Student (category)
    
    -(void)setHeight:(float)height{
        NSNumber *num = [NSNumber numberWithFloat:height];
    /*
     第一个参数是需要添加属性的对象;
     第二个参数是属性的key;
     第三个参数是属性的值,类型必须为id,所以此处height先转为NSNumber类型;
     第四个参数是使用策略,是一个枚举值,类似@property属性创建时设置的关键字,可从命名看出各枚举的意义;
     objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
     */
    objc_setAssociatedObject(self, str, num, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    //提取属性的值:
    -(float)height{
        NSNumber *number = objc_getAssociatedObject(self, str);
    return [number floatValue];
    }
    
    @end
    

    这里主要的是objc_setAssociatedObject 和 objc_getAssociatedObject两个方法的 调用。
    在ViewController中对新添加的属性进行赋值,之后再用Student类的实例对象调用这个属性的get方法。

    //增加属性
    - (IBAction)addVariable:(UIButton *)sender {
    
    student.height = 12;           //给新属性height赋值
    NSLog(@"%f",[student height]); //访问新属性值
    }
    

    打印的结果如下:

    动态增加属性.png

    四、动态的增加一个方法

    //添加一个新的方法
    - (IBAction)addNewMethod:(UIButton *)sender {
    /* 动态添加方法:
     第一个参数表示Class cls 类型;
     第二个参数表示待调用的方法名称;
     第三个参数(IMP)myAddingFunction,IMP一个函数指针,这里表示指定具体实现方法myAddingFunction;
     第四个参数表方法的参数,0代表没有参数;
     */
    class_addMethod([student class], @selector(addNewMethod), (IMP)myAddingFunction, 0);
    //调用方法 
    [student performSelector:@selector(addNewMethod)];
    }    
    
    //具体的实现(方法的内部都默认包含两个参数Class类和SEL方法,被称为隐式参数。)
    int myAddingFunction(id self, SEL _cmd){
        NSLog(@"新增的addNewMethod方法已经加入");
        return 1;
    }
    
    - (void)addNewMethod{
        NSLog(@"啦啦啦");
    }
    

    动态的添加一个方法,主要的是对runtime中class_addMethod这个方法的使用。addNewMethod是给Student对象调用的方法名,实际的调用的是函数myAddingFunction,写功能性代码也是写在这里的。
    上面二三四点的Demo地址

    五、使用runtime实现NSCoding的自动归档和解档

    这里只是处理一些常用的类型,像const void *这样类似的类型是不支持kvc的,没做处理。如果想了解推荐去看下 标哥的技术博客 。下面开始说怎样利用runtime实现NSCoding的自动归档和解档。
    新建一个项目,添加一个继承NSObject的LFUserInfo类,遵守了NSCoding协议之后,我们就可以在实现文件中实现-encodeWithCoder:方法来归档和-initWithCoder:解档。
    .h文件:

    #import <Foundation/Foundation.h>
    @interface LFUserInfo : NSObject<NSCoding>
    @property (copy  , nonatomic) NSString *username;
    @property (assign, nonatomic) int age;
    @property (assign, nonatomic) double height;
    @property (strong, nonatomic) NSNumber *phoneNumber;
    @end
    

    .m文件

    #import "LFUserInfo.h"
    #import <objc/runtime.h>
    @implementation LFUserInfo
    - (NSArray *)ignoredNames {
        return @[@"_height",@"_phoneNumber"];
    }
    //解档数据
    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        if (self = [super init]) {
            unsigned int count = 0;
            Ivar *ivarLists = class_copyIvarList([self class], &count);
            for (int i = 0 ; i<count; i++) {
                Ivar ivar = ivarLists[i];
                const char *key = ivar_getName(ivar);
                NSString *keyStr  = [NSString stringWithUTF8String:key];
            
                //忽略不需要归档的元素
                if ([[self ignoredNames] containsObject:keyStr]) {
                    continue;
                }
                //进行解档取值
                id value = [aDecoder decodeObjectForKey:keyStr];
            
                  //利用KVC对属性赋值
                    [self setValue:value forKey:keyStr];
              }
            free(ivarLists);
        }
        return self;
    }
    //归档数据
    -(void)encodeWithCoder:(NSCoder *)aCoder{
        unsigned int count = 0;
        Ivar *ivarLIst = class_copyIvarList([self class], &count);
        for (int i = 0; i<count; i++) {
            Ivar ivar = ivarLIst[i];
            const char *key = ivar_getName(ivar);
            NSString *keyStr = [NSString stringWithUTF8String:key];
            // 忽略不需要解档的属性
            if ([[self ignoredNames] containsObject:keyStr]) {
                continue;
            }
            //利用KVC取值
            id value = [self valueForKey:keyStr];
            [aCoder encodeObject:value forKey:keyStr];
        }
        free(ivarLIst);
    }
    @end
    

    之后我们在ViewController中进行归档解档操作

    归档解档后.png
    从打印的结果可以看到,我们忽视的参数,在归档解档后被忽略,其他的属性都可以归档解档操作。关于runtime实现归档解档其实不难,主要就是通过runtime动态的获取到相关的属性之后,进行操作就行了。关于归档解档的Demo在这里

    六、实现字典转模型的自动转换

    这里只是最简单的一层字典直接转模型的处理,多层数据结构的字典转模型不再本Demo范畴。
    首先我们创建几个比较正常的属性列表,譬如此Demo中,我们创建一个LFStudentmodel的model类,声明三种类型属性变量:

    @property (strong,nonatomic) NSString *name;
    @property (strong,nonatomic) NSString *schoolName;
    @property (strong,nonatomic) NSString *unitState;
    

    第二步,我们创建一个继承于NSObject的分类NSObject (Category),在.h文件中创建一个类方法

    + (instancetype)modelWithDict:(NSDictionary *)dict;
    

    在.m文件中实现这个方法:

    #import "NSObject+Category.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (Category)
    
    + (NSArray *)propertList
    {
    unsigned int count = 0;
    //获取模型属性, 返回值是所有属性的数组 objc_property_t
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    NSMutableArray *arr = [NSMutableArray array];
    //便利数组
    for (int i = 0; i< count; i++) {
        //获取属性
        objc_property_t property = propertyList[i];
        //获取属性名称
        const char *cName = property_getName(property);
        NSString *name = [[NSString alloc]initWithUTF8String:cName];
        //添加到数组中
        [arr addObject:name];
    }
        //释放属性组
        free(propertyList);
        return arr.copy;
    }
    
      + (instancetype)modelWithDict:(NSDictionary *)dict
      {
    id obj = [self new];
    // 遍历属性数组
    for (NSString *property in [self propertList]) {
        // 判断字典中是否包含这个key
        if (dict[property]) {
            // 使用 KVC 赋值
            [obj setValue:dict[property] forKey:property];
        }
    }
    return obj;
    }
    
    
    @end
    

    之后将我们这个分类的头文件导入到LFStudentmodel模型类中就可以实现,字典转模型的功效了。
    之后我们看一下字典转模型的实操
    创建一个数据管理类

    #import "LFDataManager.h"
    #import "LFStudentmodel.h"
    
    @implementation LFDataManager
    
    - (NSArray *)getSourceDataArray
    {
    NSArray *sourceArray = @[@{@"name":@"Tom",@"schoolName":@"aaa",@"unitState":@"111"},@{@"name":@"Sum",@"schoolName":@"bbb",@"unitState":@"222"},@{@"name":@"Amy",@"schoolName":@"ccc",@"unitState":@"333"},@{@"name":@"Evy",@"schoolName":@"ddd",@"unitState":@"444"},@{@"name":@"Any",@"schoolName":@"eee",@"unitState":@"555"}];
    NSMutableArray *mArray = [NSMutableArray new];
    for (int i=0; i<sourceArray.count; i++) {
        LFStudentmodel *stuModel = [LFStudentmodel modelWithDict:sourceArray[i]];
        [mArray addObject:stuModel];
    }
    return mArray;
    }
    

    最后我们在ViewController中调用展示下数据

    - (void)viewDidLoad {
    [super viewDidLoad];
    LFDataManager *manager = [LFDataManager new];
    NSArray *dataArray = [manager getSourceDataArray];
    for (int i =0; i<dataArray.count; i++) {
        LFStudentmodel *model = dataArray [i];
        NSLog(@"dataArray --- name=  %@ schoolName=  %@   unitState = %@",model.name,model.schoolName,model.unitState);
    }
    
    
    }
    

    结果如下:

    1111.png
    现在市面上很多主流解析模型的三方MJExtension、YYModel等思想也是通过runtime进行封装解析,最主要的还是setValue -- forkey这个大招来操作的。有兴趣的可以去深入了解下解析模型的三方底层实现方式。
    Demo地址
    喜欢的朋友请不吝你的👍,GitHub上点个star啥的。大神觉得写的有毛病的地方,还请指教,谢谢!

    相关文章

      网友评论

        本文标题:iOS 开发之runtime使用小结

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