iOS开发进阶之Runtime使用

4 篇文章 0 订阅
3 篇文章 0 订阅

Runtime简介

Objective-C是动态语言,即函数的实现在调用前并不确定,只有在运行中调用时才会去寻找该函数的实现,而Runtime是用于支持运行时的一套底层API。

主要内容

  • 方法调用流程
  • Runtime消息机制
  • Runtime方法交换
  • Runtime动态添加方法
  • Runtime动态添加属性
  • Runtime字典转模型

方法调用流程

首先,先来看一下OC中对象的分类:

  1. 实例对象
  2. 类对象
  3. 元类对象

每个类在内存中都有对应的一个类对象与元类对象,它们在内存中是唯一的,而类的实例对象可以是多个的,取决于程序员利用这个类实例了多少个对象。

那么,接下来我们看一下,三种对象中分别存储了什么东西:
类对象
注:只列出了部分与本节讨论有关的。

可以看到,每个对象中都有一个isa指针。那么它的用处是什么呢?其实,实例对象中的isa指向了内存中该类唯一的类对象,而类对象中的isa指针指向了内存中该类唯一的元类对象,而元类对象的isa则指向了基类的元类对象。

那么,当我们通过实例对象来调用对象方法即-号方法时,首先会通过isa找到该类对象,并在该类对象的对象方法列表中找到方法编号映射到该方法的实现,如若没有找到,则到父类对象的方法列表中查找,以此类推到基类。同样的,当我们调用类方法时,就会通过isa指针来找到元类对象,并在元类对象的类方法列表中找到方法编号映射到方法实现,找不到该方法时同类对象一样往父类上层查找直至基类。

读者可以通过以下的图来结合理解对象关系

对象关系

注:图片来源于网络,如有侵权请联系作者删除!

结合以上的内容,我们就可以来理解OC中的函数调用流程了。首先,定义了一个Person类

Person.h/
//类方法
+ (instancetype)person;
//类对象方法
- (void)eat:(NSString *)food;

Person.m/
+(instancetype)person {
    return [[Person alloc] init];
}
- (void)eat:(NSString *)food
{
    NSLog(@"eat %@",food);
}

main.m/
Person *p = [Person person];
[p eat:@"苹果"];

当我们在main.m通过[p eat]调用对象方法时,首先会通过p这个实例对象的isa指针找到Person的类对象,而后在类对象的方法编号列表中查找该方法的方法编号,找到方法编号后进行映射同方法地址列表中得到该方法的实现地址进行调用。

同样的,通过调用[Person person]来调用类方法时,步骤如上。不同的是,调用类方法使用的是Person类对象的isa指针来寻找元类对象。

讲的有些晦涩,读者可以通过此图加深理解

函数调用流程

消息机制

其实,OC的方法调用代码底层都是通过消息机制来进行实现的。那么消息机制是什么呢,又有什么理由说方法调用底层就是通过消息机制来进行实现的呢?

看一段代码

id objc = [NSObject alloc];
objc = [objc init];

这是创建并初始化一个NSObject对象的OC代码,当我们通过Clang编译器将该段代码进行转化后,我们就可以得到对应的Runtime的API。

转化方式如下:

  1. 在终端中进入该文件上一层的文件夹
  2. 输入:clang -rewrite-objc 当前文件名.后缀
  3. 回车后,当前文件夹下就会生成一个后缀名为cpp的文件,找到对应的代码片段就可以得到Runtime的实现

接下来贴上转化后的代码:

//原版
id objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
         
objc = ((id (*)(id, SEL))(void *)objc_msgSend)((id)objc, sel_registerName("init"));

//简化版
id objc = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc"));
        
objc = objc_msgSend(objc, sel_registerName("init"));

通过观察简化版,我们发现,创建一个对象调用的方法实质就是通过Runtime的消息机制来发送消息来创建的。注意::如果想要转化为简化版,必须将Xcode的消息机制严格检查关闭。关闭方法:Build Settings-搜索msg-No。

那么在日常的开发中,Runtime的消息机制的应用场景是什么呢?例如,通常没有声明的函数,在外部我们是无法调用的。但是,通过Runtime的消息机制,我们是可以实现这个需求的。假设上例中的Person的eat在.h文件中没有声明,只在.m中实现了,那么在main.m,我们就可以通过如下代码进行调用。

objc_msgSend(p, sel_registerName("eat"));

方法交换

在OC中,由于设计机制问题,我们通常是无法给系统类的方法添加新功能的。那么假设有这么一个场景,需要给UIImage类的imageNamed这个方法添加一个是否加载图片成功这个功能。那么在不改变调用方式,即还是通过调用UIImage的imageNamed这个方法的基础上添加该新功能。一般的方法是无法实现的,这时,我们就可以用Runtime来实现。

步骤如下:

  1. 在UIImage的分类中创建xxx_imageNamed,该方法中调用了imageNamed
  2. 在分类的load方法中进行方法交换

源码

UIImage+Image.m/

+ (void)load
{
    //保证线程安全
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        @autoreleasepool {
      		      //	获取系统方法
            Method m1 = class_getClassMethod(self, @selector(imageNamed:));
            //获取自定义方法
            Method m2 = class_getClassMethod(self, @selector(wwy_imageNamed:));
            //交换方法实现
            method_exchangeImplementations(m1, m2);
        };
    });
    
    
}
//自定义方法实现
+ (UIImage *)wwy_imageNamed:(NSString *)imageName {
    
    UIImage *image = [UIImage wwy_imageNamed:imageName];
    
    if(image != nil) {
        NSLog(@"图片:%@ 加载成功",imageName);
    }else {
        NSLog(@"图片:%@ 加载失败",imageName);
    }
    
    return image;
}

在分类中交换完后,在外部调用UIImage的imageNamed方法时,就会调用分类中的wwy_imageNamed方法。

动态添加方法

现在的许多软件中,很多功能需要开通会员后才能使用,例如万恶的企鹅、直播软件等。那么一般的写法是提前给每个用户都加上会员功能,然后判断用户是否为会员来决定是否给用户使用会员功能。但是,很多人一般都不会开通会员,那如此一来,提前加入会员功能就相当于鸡,而且占据了很多内存,造成了不必要的内存浪费。所以有没有一种方法是,当该用户开通会员后,才给他加上会员功能呢?答案是肯定的,就是利用Runtime动态添加方法。

main.m/

Person *p = [Person new];
[p performSelector:@selector(eat:) withObject:@"apple"];

Person.m/
//方法实现
void eat(id self,SEL _cmd,NSString *food) {
    NSLog(@"eat %@",food);
}
+(BOOL)resolveInstanceMethod:(SEL)sel {

    if(sel == NSSelectorFromString(@"eat:")) {
        class_addMethod(self, sel, (IMP)eat, "v@;@");
    }
    return YES;
}

当我们在main.m中使用performSelector方法调用eat方法时,由于Person中没有该对象方法,所以会调用Person类中的resolveInstanceMethod方法,这时,我们就可以在该方法中进行动态添加方法。同样的,如果调用的是类方法,没有该类方法会进入resolveClassMethod方法中。

实现的核心是使用Runtime的API:class_addMethod(class,sel,imp,types)。class:给哪个类对象动态添加方法就填哪个类,sel:要动态添加的方法编号,imp:动态添加的方法的实现,types:方法的信息,即反馈给编译器的方法信息,如何填写类型可以查看apple的文档。

细心的读者可能会发现,我们定义的eat方法实现中,多了两个我们并没有传入的参数:self、_cmd。这两位是什么呢,它们又有什么作用呢?其实,对于每个函数,在转化为C函数后都会给它们添加两个隐式参数,就是self:当前对象,_cmd:当前方法选择器。仔细回想对象的知识,每个对象方法都是存放在类对象的方法列表中的,而类对象只有一个。当有多个实例对象调用对象方法时,假设在对象函数中操作了实例对象的属性,那么就需要将实例对象区分开,这时self的作用就体现出来即区分每个对象。

举栗子

OC
id *objc = [NSObject alloc];
C
objc_msgsend([NSObject class],@selector(alloc))

在处理alloc函数时,底层是将OC中alloc函数转化为C语言后,self就保存了NSObject的类对象,而_cmd则保存了alloc的方法选择器。

动态添加属性

假设如果我们想给系统类添加属性,那么只能通过Runtime来实现。

NSObject+Property.h/

@property NSString *name;

NSObject+Property.m/
-(void)setName:(NSString *)name {
    
    objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name {
    
    return objc_getAssociatedObject(self, "name");
}

通过Runtime的API:objc_setAssociatedObject与objc_getAssociatedObject来进行键值绑定,从而实现给系统类动态添加属性。

字典转模型

在开发中,我们经常会使用到后台返回的数据,经过反序列化后得到一个OC的字典对象,然后根据后台的开发文档利用这个字典对模型来进行数据填充。那么问题来了,有时候我们并用不到所有后台返回的字段,那么如果预先按照开发文档在模型中添加所有字段的话,会造成内存浪费,这是我们不希望见到的,而且如果有时候字段很多,手动去创建模型的字段会很繁杂。这时,我们就可以使用Runtime来按照模型中的字段去字典中查找相应的键值对来进行数据填充,以此实现字典自动转模型。

NSObject+DictToModel.h/
//字典转模型接口
+ (instancetype)modelWithDict:(NSDictionary *)dict;

NSObject+DictToModel.m/

+ (instancetype)modelWithDict:(NSDictionary *)dict{
    
    // 创建对应类的对象
    id objc = [[self alloc] init];
    // count:成员变量总数
    unsigned int count = 0;
    // 获得成员变量列表和成员变量总数
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    for (int i = 0 ; i < count; i++) {
        // 获取成员变量
        Ivar ivar = ivarList[i];
        
        // 获取成员变量名,例如有一个age属性,此处会得到_age,如果是私有变量,则没有_,所以要使用替换,不能使用subStringFromIndex:1来获取key,不过一般也不会使用私有变量
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取key
        NSString *key = [ivarName stringByReplacingOccurrencesOfString:@"_" withString:@""];
        // 获取字典的value key:成员变量名 value:字典的值
        id value = dict[key];
        
        /* 二级转换开始 适用于模型嵌套模型的转换 */
    
        // 获取成员属性类型,例如为NSString类型,此处会得到一个 @"@\"NSString\"" 的字符串
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        
        // dict中这个属性为字典类型,而且模型中的这个键对应的值是一个模型,就需要进行二级转换
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            
            // 获取需要转换类的类对象
            Class modelClass =  NSClassFromString(ivarType);
            // 如果类名不为空则进行二级转换
            if (modelClass) {
                // 返回二级模型赋值给value
                value =  [modelClass modelWithDict:value];
            }
        }
        
        /* 二级转换结束 适用于模型嵌套模型的转换 */
        
        if (value) {
            // KVC赋值:不能传空
            [objc setValue:value forKey:key];
        }
    }
    
    // 说明:由于ARC只适用于Foundation等框架,对于Core Foundation 和 runtime 等并不适用,所以在使用带有copy、retain等字样的函数或方法时需要手动释放free()。
    free(ivarList);
    
    // 返回模型
    return objc;
    
}

附自动生成属性代码的分类

NSDictionary+GetPropertyCode.h/
//自动生成属性代码
- (void)getModelProperty;

NSDictionary+GetPropertyCode.m/

//获取模型的字段
- (void)getModelProperty {
    NSMutableString *strs = [NSMutableString new];
    
    //遍历字典的键值对
    [self enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        NSString *str;
        if([obj isKindOfClass:[NSString class]]) {
            //字符串
            str = [NSString stringWithFormat:@"@property (strong,nonatomic) NSString *%@;",key];
        }else if([obj isKindOfClass:[NSArray class]]) {
            //数组
            str = [NSString stringWithFormat:@"@property (strong,nonatomic) NSArray *%@;",key];
        }else if([obj isKindOfClass:[NSMutableArray class]]) {
            //可变数组
            str = [NSString stringWithFormat:@"@property (strong,nonatomic) NSMutableArray *%@;",key];
        }else if([obj isKindOfClass:[NSDictionary class]]) {
            //字典
            str = [NSString stringWithFormat:@"@property (strong,nonatomic) NSDictionary *%@;",key];
        }else if([obj isKindOfClass:[NSMutableDictionary class]]) {
            //可变字典
            str = [NSString stringWithFormat:@"@property (strong,nonatomic) NSMutableDictionary *%@;",key];
        }else if([obj isKindOfClass:NSClassFromString(@"__NSCFBoolean")]) {
            //bool
            str = [NSString stringWithFormat:@"@property (assign,nonatomic) BOOL %@;",key];
        }else if([obj isKindOfClass:[NSNumber class]]) {
            //NSNumber
            str = [NSString stringWithFormat:@"@property (assign,nonatomic) NSInteger %@;",key];
        }
        //添加到可变字符串中
        [strs appendString:@"\n"];
        [strs appendString:[NSString stringWithFormat:@"//\n%@\n",str]];
    }];
    NSLog(@"%@",strs);
}

字典转模型的源码如上,读者可以结合注释自行理解,这个功能还是很实用的。

总结

通过使用Runtime来实现如上的功能,不仅加深了作者对于类与对象的理解,也学习到了OC的部分设计机制。学习一个新知识点,并不需要急于求成,没有达到预期的效果,就慢慢调试,最后总是可以掌握的。以上内容均为个人学习理解,有误之处劳请各位指教。

歌曲推荐:礼物-许巍

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值