iOS面试中常常问的点 - RunTime

一. RunTime简介

我将iOS的一些学习视频书籍资料总结在“码农Style”公众号里,须要的小伙伴能够自行获取面试

想要一块儿探讨学习iOS底层原理,架构的能够加我Q_2336684744欢迎一块儿学习交流数组

RunTime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。xcode

对于C语言,函数的调用在编译的时候会决定调用哪一个函数,若是调用未实现的函数就会报错。 对于OC语言,属于动态调用过程,在编译的时候并不能决定真正调用哪一个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。在编译阶段,OC能够调用任何函数,即便这个函数并未实现,只要声明过就不会报错。bash

二. RunTime消息机制

消息机制是运行时里面最重要的机制,OC中任何方法的调用,本质都是发送消息。 使用运行时,发送消息须要导入框架<objc/message.h>而且xcode5以后,苹果不建议使用底层方法,若是想要使用运行时,须要关闭严格检查objc_msgSend的调用,BuildSetting->搜索msg 改成NO。服务器

下来看一下实例方法调用底层实现架构

Person *p = [[Person alloc] init];
[p eat];
// 底层会转化成
//SEL:方法编号,根据方法编号就能够找到对应方法的实现。
[p performSelector:@selector(eat)];
//performSelector本质即为运行时,发送消息,谁作事情就调用谁 
objc_msgSend(p, @selector(eat));
// 带参数
objc_msgSend(p, @selector(eat:),10);
复制代码

类方法的调用底层框架

// 本质是会将类名转化成类对象,初始化方法实际上是在建立类对象。
[Person eat];
// Person只是表示一个类名,并非一个真实的对象。只要是方法必需要对象去调用。
// RunTime 调用类方法一样,类方法也是类对象去调用,因此须要获取类对象,而后使用类对象去调用方法。
Class personclass = [Persion class];
[[Persion class] performSelector:@selector(eat)];
// 类对象发送消息
objc_msgSend(personclass, @selector(eat));
复制代码

**@selector (SEL):是一个SEL方法选择器。**SEL其主要做用是快速的经过方法名字查找到对应方法的函数指针,而后调用其函数。SEL其自己是一个Int类型的地址,地址中存放着方法的名字。 对于一个类中。每个方法对应着一个SEL。因此一个类中不能存在2个名称相同的方法,即便参数类型不一样,由于SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。函数

运行时发送消息的底层实现 每个类都有一个方法列表 Method List,保存这类里面全部的方法,根据SEL传入的方法编号找到方法,至关于value - key的映射。而后找到方法的实现。去方法的实现里面去实现。如图所示。 学习

运行时发送消息的底层实现

那么内部是如何动态查找对应的方法的? 首先咱们知道全部的类中都继承自NSObject类,在NSObjcet中存在一个Class的isa指针。ui

typedef struct objc_class *Class;
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
复制代码

咱们来到objc_class中查看,其中包含着类的一些基本信息。

struct objc_class {
  Class isa; // 指向metaclass
  
  Class super_class ; // 指向其父类
  const char *name ; // 类名
  long version ; // 类的版本信息,初始化默认为0,能够经过runtime函数class_setVersion和class_getVersion进行修改、读取
  long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
  long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);
  struct objc_ivar_list *ivars; // 用于存储每一个成员变量的地址
  struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
  struct objc_cache *cache; // 指向最近使用的方法的指针,用于提高效率;
  struct objc_protocol_list *protocols; // 存储该类遵照的协议
}
复制代码

下面咱们就以p实例的eat方法来看看具体消息发送以后是怎么来动态查找对应的方法的。

  1. 实例方法[p eat];底层调用[p performSelector:@selector(eat)];方法,编译器在将代码转化为objc_msgSend(p, @selector(eat));
  2. objc_msgSend函数中。首先经过pisa指针找到p对应的class。在Class中先去cache中经过SEL查找对应函数method,若是找到则经过method中的函数指针跳转到对应的函数中去执行。
  3. cache中未找到。再去methodList中查找。若能找到,则将method加入到cache中,以方便下次查找,并经过method中的函数指针跳转到对应的函数中去执行。
  4. methodlist中未找到,则去superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并经过method中的函数指针跳转到对应的函数中去执行。

三. 使用RunTime交换方法:

当系统自带的方法功能不够,须要给系统自带的方法扩展一些功能,而且保持原有的功能时,可使用RunTime交换方法实现。 这里要实现image添加图片的时候,自动判断image是否为空,若是为空则提醒图片不存在。 方法一:使用分类

+ (nullable UIImage *)xx_ccimageNamed:(NSString *)name
{
    // 加载图片    若是图片不存在则提醒或发出异常
   UIImage *image = [UIImage imageNamed:name];
    if (image == nil) {
        NSLog(@"图片不存在");
    }
    return image;
}
复制代码

缺点:每次使用都须要导入头文件,而且若是项目比较大,以前使用的方法所有须要更改。

方法二 :RunTime交换方法 交换方法的本质实际上是交换两个方法的实现,即调换xx_ccimageNamed和imageName方法,达到调用xx_ccimageNamed其实就是调用imageNamed方法的目的

那么首先须要明白方法在哪里交换,由于交换只须要进行一次,因此在分类的load方法中,当加载分类的时候交换方法便可。

+(void)load
{
    // 获取要交换的两个方法
    // 获取类方法  用Method 接受一下
    // class :获取哪一个类方法 
    // SEL :获取方法编号,根据SEL就能去对应的类找方法。
    Method imageNameMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
    // 获取第二个类方法
    Method xx_ccimageNameMrthod = class_getClassMethod([UIImage class], @selector(xx_ccimageNamed:));
    // 交换两个方法的实现 方法一 ,方法二。
    method_exchangeImplementations(imageNameMethod, xx_ccimageNameMrthod);
    // IMP其实就是 implementation的缩写:表示方法实现。
}
复制代码

交换方法内部实现:

  1. 根据SEL方法编号在Method中找到方法,两个方法都找到
  2. 交换方法的实现,指针交叉指向。如图所示:
    交换方法内部实现

注意:交换方法时候 xx_ccimageNamed方法中就不能再调用imageNamed方法了,由于调用imageNamed方法实质上至关于调用 xx_ccimageNamed方法,会循环引用形成死循环。

RunTime也提供了获取对象方法和方法实现的方法。

// 获取方法的实现
class_getMethodImplementation(<#__unsafe_unretained Class cls#>, <#SEL name#>) 
// 获取对象方法
class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
复制代码

此时,当调用imageNamed:方法的时候就会调用xx_ccimageNamed:方法,为image添加图片,并判断图片是否存在,若是不存在则提醒图片不存在。

四. 动态添加方法

若是一个类方法很是多,其中可能许多方法暂时用不到。而加载类方法到内存的时候须要给每一个方法生成映射表,又比较耗费资源。此时可使用RunTime动态添加方法

动态给某个类添加方法,至关于懒加载机制,类中许多方法暂时用不到,那么就先不加载,等用到的时候再去加载方法。

动态添加方法的方法: 首先咱们先不实现对象方法,当调用performSelector: 方法的时候,再去动态加载方法。 这里同上建立Person类,使用performSelector: 调用Person类对象的eat方法。

Person *p = [[Person alloc]init];
// 当调用 P中没有实现的方法时,动态加载方法
[p performSelector:@selector(eat)];
复制代码

此时编译的时候是不会报错的,程序运行时才会报错,由于Person类中并无实现eat方法,当去类中的Method List中发现找不到eat方法,会报错找不到eat方法。

报错信息:未被选择器发送到实例

而当找不到对应的方法时就会来到拦截调用,在找不到调用的方法程序崩溃以前调用的方法。 当调用了没有实现的对象方法的时,就会调用**+(BOOL)resolveInstanceMethod:(SEL)sel方法。 当调用了没有实现的类方法的时候,就会调用+(BOOL)resolveClassMethod:(SEL)sel**方法。

首先咱们来到API中看一下苹果的说明,搜索 Dynamic Method Resolution 来到动态方法解析。

Dynamic Method Resolution

Dynamic Method Resolution的API中已经讲解的很清晰,咱们能够实现方法resolveInstanceMethod:或者resolveClassMethod:方法,动态的给实例方法或者类方法添加方法和方法实现。

因此经过这两个方法就能够知道哪些方法没有实现,从而动态添加方法。参数sel即表示没有实现的方法。

一个objective - C方法最终都是一个C函数,默认任何一个方法都有两个参数。 self : 方法调用者 _cmd : 调用方法编号。咱们可使用函数class_addMethod为类添加一个方法以及实现。

这里仿照API给的例子,动态的为P实例添加eat对象

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    // 动态添加eat方法
    // 首先判断sel是否是eat方法 也能够转化成字符串进行比较。    
    if (sel == @selector(eat)) {
    /** 
     第一个参数: cls:给哪一个类添加方法
     第二个参数: SEL name:添加方法的编号
     第三个参数: IMP imp: 方法的实现,函数入口,函数名可与方法名不一样(建议与方法名相同)
     第四个参数: types :方法类型,须要用特定符号,参考API
     */
      class_addMethod(self, sel, (IMP)eat , "v@:");
        // 处理完返回YES
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
复制代码

重点来看一下class_addMethod方法

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
复制代码

class_addMethod中的四个参数。第一,二个参数比较好理解,重点是第三,四个参数。

  1. cls : 表示给哪一个类添加方法,这里要给Person类添加方法,self即表明Person。
  2. SEL name : 表示添加方法的编号。由于这里只有一个方法须要动态添加,而且以前经过判断肯定sel就是eat方法,因此这里可使用sel。
  3. IMP imp : 表示方法的实现,函数入口,函数名可与方法名不一样(建议与方法名相同)须要本身来实现这个函数。每个方法都默认带有两个隐式参数 self : 方法调用者 _cmd : 调用方法的标号,能够写也能够不写。
void eat(id self ,SEL _cmd)
{
      // 实现内容
      NSLog(@"%@的%@方法动态实现了",self,NSStringFromSelector(_cmd));
}
复制代码
  1. types : 表示方法类型,须要用特定符号。系统提供的例子中使用的是**"v@:",咱们来到API中看看"v@:"**指定的方法是什么类型的。
    Objective-C type encodings
    从图中能够看出

v -> void 表示无返回值 @ -> object 表示id参数 : -> method selector 表示SEL

至此已经完成了P实例eat方法的动态添加。当P调用eat方法时输出

p调用eat方法时输出

动态添加有参数的方法 若是是有参数的方法,须要对方法的实现和class_addMethod方法内方法类型参数作一些修改。 方法实现:由于在C语言函数中,因此对象参数类型只能用id代替。 方法类型参数:由于添加了一个id参数,因此方法类型应该为**"v@:@"** 来看一下代码

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat:)) {
        class_addMethod(self, sel, (IMP)aaaa , "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void aaaa(id self ,SEL _cmd,id Num)
{
    // 实现内容
    NSLog(@"%@的%@方法动态实现了,参数为%@",self,NSStringFromSelector(_cmd),Num);
}
复制代码

调用eat:函数

Person *p = [[Person alloc]init];
[p performSelector:@selector(eat:)withObject:@"xx_cc"];
复制代码

输出为

p调用eat:方法时输出

五. RunTime动态添加属性

使用RunTime给系统的类添加属性,首先须要了解对象与属性的关系。

对象与属性的关系

对象一开始初始化的时候其属性name为nil,给属性赋值其实就是让name属性指向一块存储字符串的内存,使这个对象的属性跟这块内存产生一种关联,我的理解对象的属性就是一个指针,指向一块内存区域。

那么若是想动态的添加属性,其实就是动态的产生某种关联就行了。而想要给系统的类添加属性,只能经过分类。

这里给NSObject添加name属性,建立NSObject的分类 咱们可使用@property给分类添加属性

@property(nonatomic,strong)NSString *name;
复制代码

虽然在分类中能够写@property 添加属性,可是不会自动生成私有属性,也不会生成set,get方法的实现,只会生成set,get的声明,须要咱们本身去实现。

方法一:咱们能够经过使用静态全局变量给分类添加属性

static NSString *_name;
-(void)setName:(NSString *)name
{
    _name = name;
}
-(NSString *)name
{
    return _name;
}
复制代码

可是这样_name静态全局变量与类并无关联,不管对象建立与销毁,只要程序在运行_name变量就存在,并非真正意义上的属性。

方法二:使用RunTime动态添加属性 RunTime提供了动态添加属性和得到属性的方法。

-(void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
    return objc_getAssociatedObject(self, @"name");    
}
复制代码
  1. 动态添加属性
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
复制代码

参数一:id object : 给哪一个对象添加属性,这里要给本身添加属性,用self。 参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在**objc_getAssociatedObject中经过次key得到属性的值并返回。 参数三:id value** : 关联的值,也就是set方法传入的值给属性去保存。 参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。 有如下几种

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一个弱引用相关联的对象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相关的对象被复制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相关对象的强引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相关的对象被复制,原子性   
};
复制代码
  1. 得到属性
objc_getAssociatedObject(id object, const void *key);
复制代码

参数一:id object : 获取哪一个对象里面的关联的属性。 参数二:void * == id key : 什么属性,与**objc_setAssociatedObject**中的key相对应,即经过key值取出value。

此时已经成功给NSObject添加name属性,而且NSObject对象能够经过点语法为属性赋值。

NSObject *objc = [[NSObject alloc]init];
objc.name = @"xx_cc";
NSLog(@"%@",objc.name);
复制代码

六. RunTime字典转模型

为了方便之后重用,这里经过给NSObject添加分类,声明并实现使用RunTime字典转模型的类方法。

+ (instancetype)modelWithDict:(NSDictionary *)dict
复制代码

首先来看一下KVC字典转模型和RunTime字典转模型的区别

KVC:KVC字典转模型实现原理是遍历字典中全部Key,而后去模型中查找相对应的属性名,要求属性名与Key必须一一对应,字典中全部key必须在模型中存在。 RunTime:RunTime字典转模型实现原理是遍历模型中的全部属性名,而后去字典查找相对应的Key,也就是以模型为准,模型中有哪些属性,就去字典中找那些属性。

RunTime字典转模型的优势:当服务器返回的数据过多,而咱们只使用其中不多一部分时,没有用的属性就没有必要定义成属性浪费没必要要的资源。只保存最有用的属性便可。

RunTime字典转模型过程 首先须要了解,属性定义在类里面,那么类里面就有一个属性列表,属性列表以数组的形式存在,根据属性列表就能够得到类里面的全部属性名,因此遍历属性列表,也就能够遍历模型中的全部属性名。 因此RunTime字典转模型过程就很清晰了。

  1. 建立模型对象
id objc = [[self alloc] init];
复制代码
  1. 使用**class_copyIvarList**方法拷贝成员属性列表
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
复制代码

参数一:__unsafe_unretained Class cls : 获取哪一个类的成员属性列表。这里是self,由于谁调用分类中类方法,谁就是self。 参数二:unsigned int *outCount : 无符号int型指针,这里建立unsigned int型count,&count就是他的地址,保证在方法中能够拿到count的地址为count赋值。传出来的值为成员属性总数。 返回值:Ivar * : 返回的是一个Ivar类型的指针 。指针默认指向的是数组的第0个元素,指针+1会向高地址移动一个Ivar单位的字节,也就是指向第一个元素。Ivar表示成员属性。 3. 遍历成员属性列表,得到属性列表

for (int i = 0 ; i < count; i++) {
        // 获取成员属性
        Ivar ivar = ivarList[i];
}
复制代码
  1. 使用**ivar_getName(ivar)**得到成员属性名,由于成员属性名返回的是C语言字符串,将其转化成OC字符串
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
复制代码

经过**ivar_getTypeEncoding(ivar)**也能够得到成员属性类型。 5. 由于得到的是成员属性名,是带_的成员属性,因此须要将下划线去掉,得到属性名,也就是字典的key。

// 获取key
NSString *key = [propertyName substringFromIndex:1];
复制代码
  1. 获取字典中key对应的Value。
// 获取字典的value
id value = dict[key];
复制代码
  1. 给模型属性赋值,并将模型返回
if (value) {
 // KVC赋值:不能传空
[objc setValue:value forKey:key];
}
return objc;
复制代码

至此已成功将字典转为模型。

七. RunTime字典转模型的二级转换

在开发过程当中常常用到模型嵌套,也就是模型中还有一个模型,这里尝试用RunTime进行模型的二级转换,实现思路其实比较简单清晰。

  1. 首先得到一级模型中的成员属性的类型
// 成员属性类型
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
复制代码
  1. 判断当一级字典中的value是字典,而且一级模型中的成员属性类型不是NSDictionary的时候才须要进行二级转化。 首先value是字典才进行转化是必须的,由于咱们一般将字典转化为模型,其次,成员属性类型不是系统类,说明成员属性是咱们自定义的类,也就是要转化的二级模型。而当成员属性类型就是NSDictionary的话就代表,咱们本就想让成员属性是一个字典,不须要进行模型的转换。
id value = dict[key];
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) 
{ 
      // 进行二级转换。
}
复制代码
  1. 获取要转换的模型类型,这里须要对propertyType成员属性类型作一些处理,由于propertyType返回给咱们成员属性类型的是**@\"Mode\",咱们须要对他进行截取为Mode**。这里须要注意的是\只是转义符,不占位。
// @\"Mode\"去掉前面的@\" NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex:range.location + range.length];
// Mode\"去掉后面的\" range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringToIndex:range.location];
复制代码
  1. 获取须要转换类的类对象,将字符串转化为类名。
Class modelClass =  NSClassFromString(propertyType);
复制代码
  1. 判断若是类名不为空则调用分类的modelWithDict方法,传value字典,进行二级模型转换,返回二级模型在赋值给value。
if (modelClass) {
      value =  [modelClass modelWithDict:value];
}  
复制代码

这里可能有些绕,从新理一下,咱们经过判断value是字典而且须要进行二级转换,而后将value字典转化为模型返回,并从新赋值给value,最后给一级模型中相对应的key赋值模型value便可完成二级字典对模型的转换。

最后附上二级转换的完整方法

+ (instancetype)modelWithDict:(NSDictionary *)dict{
    // 1.建立对应类的对象
    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];
        // 获取成员名
       NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取key
        NSString *key = [propertyName substringFromIndex:1];
        // 获取字典的value key:属性名 value:字典的值
        id value = dict[key];
        // 获取成员属性类型
        NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // 二级转换
        // value值是字典而且成员属性的类型不是字典,才须要转换成模型
        if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
            // 进行二级转换
            // 获取二级模型类型进行字符串截取,转换为类名
            NSRange range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringFromIndex:range.location + range.length];
            range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringToIndex:range.location];
            // 获取须要转换类的类对象
           Class modelClass =  NSClassFromString(propertyType);
           // 若是类名不为空则进行二级转换
            if (modelClass) {
                // 返回二级模型赋值给value
                value =  [modelClass modelWithDict:value];
            }
        }
        if (value) {
            // KVC赋值:不能传空
            [objc setValue:value forKey:key];
        }
    }
    // 返回模型
    return objc;
}
复制代码

以上只是对RunTime浅显的理解,足以应付iOS面试过程当中Runtime的一些问题。


我将iOS的一些学习视频书籍资料总结在“码农Style”公众号里,须要的小伙伴能够自行获取。

扫码关注
相关文章
相关标签/搜索