iOS 模块详解—「Runtime」

前言,文章是转载的,因为之前收藏的,今天突然发现没了。不知道什么原因,搜索不到了,有几篇同样转载的,但是代码没整理,看起来很难受,所以我来整理一下。好的文章应该让更多人看见。

引导

对于从事 iOS 开发人员来说,所有的人都会答出「 Runtime 是运行时 」,什么情况下用 Runtime ?,大部分人能说出「 给分类动态添加属性 || 交换方法 」,再问一句「 Runtime 消息机制的调用流程 || 能体现 Runtime 强大之处的应用场景 」,到这,能知道答案的寥寥无几,很少有人会说到 “黑魔法” 这三个字。
Runtime 是 iOS 编程中比较难的模块,想要深入学习 OC,那 Runtime 是你必须要熟练掌握的东西,下面是我对 Runtime 的整理,从零开始,由浅入深,并且带了几个 Runtime 实际开发应用场景 --> 大神可选择性路过「思想」。


iOS 模块详解—「Runtime」_第1张图片
image.png

在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,可留言指正或是补充,以提高文章质量@白开水ln原著;

目录:

  1. runtime 概念
  2. runtime 消息机制
  3. runtime 方法调用流程「消息机制」
  4. runtime 运行时常见作用
  5. runtime 常用开发应用场景「工作掌握」
    1.runtime 交换方法
    2.UITextField占位文字颜色(工具类)
    3.runtime 给分类动态添加属性
    4.runtime 字典转模型(Runtime 考虑三种情况实现)
  6. runtime 运行时其它作用「面试熟悉」
    1.动态添加方法
    2.动态变量控制
    3.实现NSCoding的自动归档和解档
    4.runtime 下Class的各项操作
    5.runtime 几个参数概念
  7. 什么是 method swizzling(俗称黑魔法)
  8. 最后一道面试题的注解
  9. runtime 模块博文推荐(❤️数量较多)
  10. runtime & runloop 面试最常问到的题整理【建议看】
  11. Demo 重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。
  12. runtime.h
  13. SourceCode 、 ToolsClass、WechatPublic-Codeidea

runtime 概念

Objective-C 是基于 C 的,它为 C 添加了面向对象的特性。它将很多静态语言在编译和链接时期做的事放到了 runtime 运行时来处理,可以说 runtime 是我们 Objective-C 幕后工作者。

  • runtime(简称运行时),是一套 纯C(C和汇编写的) 的API。而 OC 就是 运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。

  • 对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。

  • OC的函数调用成为消息发送,属于 动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

  • 事实证明:在编译阶段,OC 可以 调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而 C 语言 调用未实现的函数 就会报错。

runtime 消息机制

我们写 OC 代码,它在运行的时候也是转换成了 runtime 方式运行的。任何方法调用本质:就是发送一个消息(用 runtime发送消息,OC 底层实现通过 runtime 实现)。

消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
每一个 OC 的方法,底层必然有一个与之对应的 runtime 方法。


image.png

OC-->runtime
简单示例:
验证:方法调用,是否真的是转换为消息机制?

  • 必须要导入头文件 #import

  • 注解1:我们导入系统的头文件,一般用尖括号。

  • 注解2:OC 解决消息机制方法提示步骤【查找build setting -> 搜索msg -> objc_msgSend(YES --> NO)】

  • 注解3:最终生成消息机制,编译器做的事情,最终代码,需要把当前代码重新编译,用xcode编译器,【clang -rewrite-objc main.m 查看最终生成代码】,示例:cd main.m --> 输入前面指令,就会生成 .opp文件(C++代码)

  • 注解4:这里一般不会直接导入


    image.png
  • 示例代码:OC 方法-->runtime 方法

说明: eat(无参) 和 run(有参) 是 Person模型类中的私有方法「可以帮我调用私有方法」;
 
// Person *p = [Person alloc];
// 底层的实际写法
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
 
// p = [p init];
p = objc_msgSend(p, sel_registerName("init"));
 
// 调用对象方法(本质:让对象发送消息)
//[p eat];
 
// 本质:让类对象发送消息
objc_msgSend(p, @selector(eat));
objc_msgSend([Person class], @selector(run:),20);
 
//--------------------------- <#我是分割线#> ------------------------------//
// 也许下面这种好理解一点
 
// id objc = [NSObject alloc];
id objc = objc_msgSend([NSObject class], @selector(alloc));
 
// objc = [objc init];
objc = objc_msgSend(objc, @selector(init));

objc_msgSend 参数概念

/
 objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>)

 1、objc_msgSend
    这是个最基本的用于发送消息的函数。
    其实编译器会根据情况在`objc_msgSend`, `objc_msgSend_stret`,,`objc_msgSendSuper`, 或 `objc_msgSendSuper_stret` 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有 `Super` 的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有`stret`的函数。
 
 2、SEL
    `objc_msgSend`函数第二个参数类型为`SEL`,它是`selector`在Objc中的表示类型(Swift中是Selector类)。`selector`是方法选择器,可以理解为区分方法的 `ID`,而这个 `ID` 的数据结构是`SEL`:
    `typedef struct objc_selector *SEL;`
    其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令`@selector()``或者 Runtime` 系统的`sel_registerName`函数来获得一个`SEL`类型的方法选择器。
 
 3、id
    `objc_msgSend`第一个参数类型为`id`,大家对它都不陌生,它是一个指向类实例的指针:
    `typedef struct objc_object *id;`
    那`objc_object`又是啥呢:
    `struct objc_object { Class isa; };`
    `objc_object`结构体包含一个`isa`指针,根据`isa`指针就可以顺藤摸瓜找到对象所属的类。
 */


runtime 方法调用流程「消息机制」

面试:消息机制方法调用流程

  • 怎么去调用eat方法,对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。

  • 1.OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。

  • 2.注册方法编号(这里用方法编号的好处,可以快速查找)。

  • 3.根据方法编号去查找对应方法。

  • 4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。

  • 补充:一个objc 对象的 isa 的指针指向什么?有什么作用?

  • 每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。

runtime 常见作用

  • 动态交换两个方法的实现

  • 动态添加属性

  • 实现字典转模型的自动转换

  • 发送消息

  • 动态添加方法

  • 拦截并替换方法

  • 实现 NSCoding 的自动归档和解档

runtime 常用开发应用场景「工作掌握」

runtime 交换方法

应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。

需求:加载一张图片直接用[UIImage imageNamed:@"image"];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。

  • 方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)

  • 方案二:使用 runtime,交换方法.
    实现步骤

  • 1.给系统的方法添加分类

  • 2.自己实现一个带有扩展功能的方法

  • 3.交换方法,只需要交换一次,
    案例代码:方法+调用+打印输出


- (void)viewDidLoad {
    [super viewDidLoad];
    // 方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
    UIImage *image = [UIImage imageNamed:@"123"];
}
 
#import 
@implementation UIImage (Image)
/**
 load方法: 把类加载进内存的时候调用,只会调用一次
 方法应先交换,再去调用
 */
+ (void)load {
    
    // 1.获取 imageNamed方法地址
    // class_getClassMethod(获取某个类的方法)
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    // 2.获取 ln_imageNamed方法地址
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
    
    // 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
    method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}
 
/**
 看清楚下面是不会有死循环的
 调用 imageNamed => ln_imageNamed
 调用 ln_imageNamed => imageNamed
 */
// 加载图片 且 带判断是否加载成功
+ (UIImage *)ln_imageNamed:(NSString *)name {
    
    UIImage *image = [UIImage ln_imageNamed:name];
    if (image) {
        NSLog(@"runtime添加额外功能--加载成功");
    } else {
        NSLog(@"runtime添加额外功能--加载失败");
    }
    return image;
}
 
/**
 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
 所以第二步,我们要 自己实现一个带有扩展功能的方法.
 + (UIImage *)imageNamed:(NSString *)name {
 
 }
 */
@end
 
 
// 打印输出
2016-02-17 17:52:14.693 runtime[12761:543574] runtime添加额外功能--加载成功

总结:我们所做的就是在方法调用流程第三步的时候,交换两个方法地址指向。而且我们改变指向要在系统的imageNamed:方法调用前,所以将代码写在了分类的load方法里。最后当运行的时候系统的方法就会去找我们的方法的实现。

runtime 给分类动态添加属性

原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。
注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。

需求:给系统 NSObject 类动态添加属性 name 字符串。

案例代码:方法+调用+打印

@interface NSObject (Property)
 
// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性
@property NSString *name;
@property NSString *height;
@end
 
@implementation NSObject (Property)
 
- (void)setName:(NSString *)name {
    
    // objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
    // object:给哪个对象添加属性
    // key:属性名称
    // value:属性值
    // policy:保存策略
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
 
- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}
 
// 调用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"runtime动态添加属性name==%@",objc.name);
 
// 打印输出
2016-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123

总结:其实,给属性赋值的本质,就是让属性与一个对象产生关联,所以要给NSObject的分类的name属性赋值就是让name和NSObject产生关联,而runtime可以做到这一点。

runtime 字典转模型

字典转模型的方式:

  • 一个一个的给模型属性赋值(初学者)。

  • 字典转模型KVC实现

  • KVC 字典转模型弊端:必须保证,模型中的属性和字典中的key 一一对应。

  • 如果不一致,就会调用[ setValue:forUndefinedKey:] 报key找不到的错。

  • 分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。

  • 解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC,字典转模型了。

  • 字典转模型 Runtime 实现

  • 思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不一定要全部取出来)。

  • 考虑情况:

    • 1.当字典的key和模型的属性匹配不上。
    • 2.模型中嵌套模型(模型属性是另外一个模型对象)。
    • 3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。
  • 注解:根据上面的三种特殊情况,先是字典的key和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可。考虑三种情况下面一一注解

  • 步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。

  • MJExtension 字典转模型实现

  • 底层也是对 runtime 的封装,才可以把一个模型中所有属性遍历出来。(你之所以看不懂,是 MJ 封装了很多层而已_.)。

这里针对字典转模型 KVC 实现,就不做详解了,如果你 对 KVC 详解使用或是实现原理 不是很清楚的,可以参考 实用「KVC编码 & KVO监听

字典转模型 Runtime 方式实现:
说明:下面这个示例,是考虑三种情况包含在内的转换示例,具体可以看图上的注解

image.png

Runtime 字典转模型
1、runtime 字典转模型-->字典的 key 和模型的属性不匹配「模型属性数量大于字典键值对数」,这种情况处理如下:

// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 思路:遍历模型中所有属性->使用运行时
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 1.创建对应的对象
    id objc = [[self alloc] init];
    
    // 2.利用runtime给对象中的属性赋值
    /**
     class_copyIvarList: 获取类中的所有成员变量
     Ivar:成员变量
     第一个参数:表示获取哪个类中的成员变量
     第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值
     返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
     count: 成员变量个数
     */
    unsigned int count = 0;
    // 获取类中的所有成员变量
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量
        Ivar ivar = ivarList[i];
        
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];
        
        // 【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】
        // 而报错 (could not set nil as the value for the key age.)
        if (value) {
            // 给模型中属性赋值
            [objc setValue:value forKey:key];
        }
 
    }
   
    return objc;
}

注解:这里在获取模型类中的所有属性名,是采取 class_copyIvarList 先获取成员变量(以下划线开头) ,然后再处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取) 得到属性名。

原因:Ivar:成员变量,以下划线开头,Property 属性
获取类里面属性 class_copyPropertyList
获取类中的所有成员变量 class_copyIvarList

{
    int _a; // 成员变量
}
 
@property (nonatomic, assign) NSInteger attitudes_count; // 属性
 
这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;

使用runtime字典转模型获取模型属性名的时候,最好获取成员属性名Ivar因为可能会有个属性是没有setter和getter方法的。

2、runtime 字典转模型-->模型中嵌套模型「模型属性是另外一个模型对象」,这种情况处理如下:

+ (instancetype)modelWithDict2:(NSDictionary *)dict
{
    // 1.创建对应的对象
    id objc = [[self alloc] init];
    
    // 2.利用runtime给对象中的属性赋值
    unsigned int count = 0;
    // 获取类中的所有成员变量
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量
        Ivar ivar = ivarList[i];
        
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取成员变量类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        // 替换: @\"User\" -> User
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        
        // 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];
        
        //--------------------------- <#我是分割线#> ------------------------------//
        //
        // 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
        // 判断下value是否是字典,并且是自定义对象才需要转换
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            
            // 字典转换成模型 userDict => User模型, 转换成哪个模型
            // 根据字符串类名生成类对象
            Class modelClass = NSClassFromString(ivarType);
            
            if (modelClass) { // 有对应的模型才需要转
                // 把字典转模型
                value = [modelClass modelWithDict2:value];
            }
        }
        
        // 给模型中属性赋值
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

3、runtime 字典转模型-->数组中装着模型「模型的属性是一个数组,数组中是字典模型对象」,这种情况处理如下:

// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 思路:遍历模型中所有属性->使用运行时
+ (instancetype)modelWithDict3:(NSDictionary *)dict
{
    // 1.创建对应的对象
    id objc = [[self alloc] init];
    
    // 2.利用runtime给对象中的属性赋值
    unsigned int count = 0;
    // 获取类中的所有成员变量
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量
        Ivar ivar = ivarList[i];
        
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];
        
        
        //--------------------------- <#我是分割线#> ------------------------------//
        //
        
        // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
        // 判断值是否是数组
        if ([value isKindOfClass:[NSArray class]]) {
            // 判断对应类有没有实现字典数组转模型数组的协议
            // arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                
                // 转换成id类型,就能调用任何对象的方法
                id idSelf = self;
                
                // 获取数组中字典对应的模型
                NSString *type =  [idSelf arrayContainModelClass][key];
                
                // 生成模型
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍历字典数组,生成模型数组
                for (NSDictionary *dict in value) {
                    // 字典转模型
                    id model =  [classModel modelWithDict3:dict];
                    [arrM addObject:model];
                }
                
                // 把模型数组赋值给value
                value = arrM;
                
            }
        }
 
        // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil,而报错
        if (value) {
            // 给模型中属性赋值
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}
image.png

runtime字典转模型-->数组中装着模型 打印输出
总结:我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。

这里提到的你如果不是很清楚,建议参考我的Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。

runtime 其它作用「面试熟悉」

动态添加方法
应用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。

注解:OC 中我们很习惯的会用懒加载,当用到的时候才去加载它,但是实际上只要一个类实现了某个方法,就会被加载进内存。当我们不想加载这么多方法的时候,就会使用到 runtime 动态的添加方法。

需求:runtime 动态添加方法处理调用一个未实现的方法 和 去除报错。

案例代码:方法+调用+打印输出

- (void)viewDidLoad {
    [super viewDidLoad];   
    Person *p = [[Person alloc] init];
    // 默认person,没有实现run:方法,可以通过performSelector调用,但是会报错。
    // 动态添加方法就不会报错
    [p performSelector:@selector(run:) withObject:@10];
}
 
@implementation Person
// 没有返回值,1个参数
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@米", meter);
}
 
// 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
// 什么时候调用:只要一个对象调用了一个未实现的方法就会调用这个方法,进行处理
// 作用:动态添加方法,处理未实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"run:")) {
        // 动态添加run方法
        // class: 给哪个类添加方法
        // SEL: 添加哪个方法,即添加方法的方法编号
        // IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
        // type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)aaa, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end
 
// 打印输出
2016-02-17 19:05:03.917 runtime[12761:543574] runtime动态添加方法--跑了10米

动态变量控制

现在有一个Person类,创建 xiaoming对象

  • 动态获取 XiaoMing 类中的所有属性 [当然包括私有]
Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);
  • 遍历属性找到对应name字段
const char *varName = ivar_getName(var);
  • 修改对应的字段值成20
object_setIvar(self.xiaoMing, var, @"20");
  • 代码参考
-(void)answer{
     unsigned int count = 0;
     Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
     for (int i = 0; i

实现NSCoding的自动归档和解档

如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。

假设现在有一个Movie类,有3个属性。先看下 .h文件

// Movie.h文件
//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding
@interface Movie : NSObject
 
@property (nonatomic, copy) NSString *movieId;
@property (nonatomic, copy) NSString *movieName;
@property (nonatomic, copy) NSString *pic_url;
@end

如果是正常写法, .m 文件应该是这样的:

// Movie.m文件
@implementation Movie
 
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:_movieId forKey:@"id"];
    [aCoder encodeObject:_movieName forKey:@"name"];
    [aCoder encodeObject:_pic_url forKey:@"url"];
 
}
 
- (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init]) {
        self.movieId = [aDecoder decodeObjectForKey:@"id"];
        self.movieName = [aDecoder decodeObjectForKey:@"name"];
        self.pic_url = [aDecoder decodeObjectForKey:@"url"];
    }
    return self;
}
@end

如果这里有100个属性,那么我们也只能把100个属性都给写一遍吗。

不过你会使用runtime后,这里就有更简便的方法,如下。


#import "Movie.h"
#import 
@implementation Movie
 
- (void)encodeWithCoder:(NSCoder *)encoder
 
{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([Movie class], &count);
 
    for (int i = 0; i

这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,代码有点多,
好说下面看看更加简便的方法:两句代码搞定。

#import "Movie.h"
#import 
 
#define encodeRuntime(A) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i

优化:上面是encodeWithCoderinitWithCoder这两个方法抽成宏。我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。

runtime 下Class的各项操作

下面是 runtime 下Class的常见方法 及 带有使用示例代码。各项操作,原著 http://www.jianshu.com/p/46dd81402f63

unsigned int count;

  • 获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
 for (unsigned int i=0; i%@", [NSString stringWithUTF8String:propertyName]);
 }
  • 获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
 for (unsigned int i; i%@", NSStringFromSelector(method_getName(method)));
 }
  • 获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
  for (unsigned int i; i%@", [NSString stringWithUTF8String:ivarName]);
  }
  • 获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
  for (unsigned int i; i%@", [NSString stringWithUTF8String:protocolName]);
  }

现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法

  • 获得类方法
Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
  • 获得实例方法
Class PersonClass = object_getClass([xiaoming class]);
SEL oriSEL = @selector(test2);
Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
  • 添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));

  • 替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

  • 交换两个方法的实现
method_exchangeImplementations(oriMethod, cusMethod);
  • 常用方法
附:上面有提到写常用示例,这里再总结下 ~
 
    // 得到类的所有方法
    Method *allMethods = class_copyMethodList([Person class], &count);
    // 得到所有成员变量
    Ivar *allVariables = class_copyIvarList([Person class], &count);
    // 得到所有属性
    objc_property_t *properties = class_copyPropertyList([Person class], &count);
    // 根据名字得到类变量的Ivar指针,但是这个在OC中好像毫无意义
Ivar oneCVIvar = class_getClassVariable([Person class], name);
    // 根据名字得到实例变量的Ivar指针
    Ivar oneIVIvar = class_getInstanceVariable([Person class], name);
    // 找到后可以直接对私有变量赋值
    object_setIvar(_per, oneIVIvar, @"Mike");//强制修改name属性
    /* 动态添加方法:
     第一个参数表示Class cls 类型;
     第二个参数表示待调用的方法名称;
     第三个参数(IMP)myAddingFunction,IMP是一个函数指针,这里表示指定具体实现方法myAddingFunction;
     第四个参数表方法的参数,0代表没有参数;
     */
    class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);
    // 交换两个方法
    method_exchangeImplementations(method1, method2);
 
// 关联两个对象
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
/*
 id object                     :表示关联者,是一个对象,变量名理所当然也是object
 const void *key               :获取被关联者的索引key
 id value                      :被关联者,这里是一个block
 objc_AssociationPolicy policy : 关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
*/
 
 
// 获得某个类的类方法
Method class_getClassMethod(Class cls , SEL name)
 
// 获得某个类的实例对象方法
Method class_getInstanceMethod(Class cls , SEL name)
 
// 交换两个方法的实现
void method_exchangeImplementations(Method m1 , Method m2)
 
// 将某个值跟某个对象关联起来,将某个值存储到某个对象中
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
 
// 利用参数key 将对象object中存储的对应值取出来
id objc_getAssociatedObject(id object , const void *key)
 
// 获得某个类的所有成员变量(outCount 会返回成员变量的总数)
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
 
// 获得成员变量的名字
const char *ivar_getName(Ivar v)
 
// 获得成员变量的类型
const char *ivar_getTypeEndcoding(Ivar v)
 
// 获取类里面所有方法
class_copyMethodList(__unsafe_unretained Class cls, unsigned int *outCount)// 本质:创建谁的对象
 
// 获取类里面属性
class_copyPropertyList(__unsafe_unretained Class cls, unsigned int *outCount)

runtime 几个参数概念

以上的几种方法应该算是runtime在实际场景中所应用的大部分的情况了,平常的编码中差不多足够用了。
这里在对 runtime 几个参数概念,做一简单说明

1、objc_msgSend
这是个最基本的用于发送消息的函数。
其实编译器会根据情况在objc_msgSend, objc_msgSend_stret,,objc_msgSendSuper, 或 objc_msgSendSuper_stret 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有 Super 的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret的函数。

2、SEL
objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()``或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

3、id
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

4、runtime.h里Class的定义

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;//每个Class都有一个isa指针
    
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;//父类
    const char *name                                         OBJC2_UNAVAILABLE;//类名
    long version                                             OBJC2_UNAVAILABLE;//类版本
    long info                                                OBJC2_UNAVAILABLE;//!*!供运行期使用的一些位标识。如:CLS_CLASS (0x1L)表示该类为普通class; CLS_META(0x2L)表示该类为metaclass等(runtime.h中有详细列出)
    long instance_size                                       OBJC2_UNAVAILABLE;//实例大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;//存储每个实例变量的内存地址
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;//!*!根据info的信息确定是类还是实例,运行什么函数方法等
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;//缓存
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;//协议
#endif
    
} OBJC2_UNAVAILABLE;

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
在objc_class结构体中:``ivars是objc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是Category实现的原理。
什么是 method swizzling(俗称黑魔法)

  • 简单说就是进行方法交换

  • 在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的

  • 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。

image.png

selector --> 对应的IMP

  • 交换方法的几种实现方式
  • 利用 method_exchangeImplementations 交换两个方法的实现
  • 利用 class_replaceMethod 替换方法的实现
  • 利用 method_setImplementation 来直接设置某个方法的IMP。


    image.png

这里可以参考简友这篇:【Runtime Method Swizzling开发实例汇总】http://www.jianshu.com/p/f6dad8e1b848

最后一道面试题的注解

下面的代码输出什么?

@implementation Son : NSObject
- (id)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

先思考一下,会打印出来什么❓
关注我的更多干货分享 _.

答案:都输出 Son

  • class 获取当前方法的调用者的类,superClass 获取当前方法的调用者的父类,super 仅仅是一个编译指示器,就是给编译器看的,不是一个指针。

  • 本质:只要编译器看到super这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用
    这个题目主要是考察关于objc中对 self 和 super 的理解:

  • self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者

  • 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;

  • 而当使用 super时,则从父类的方法列表中开始找。然后调用父类的这个方法

  • 调用 [self class] 时,会转化成 objc_msgSend 函数

id objc_msgSend(id self, SEL op, ...)
- 调用 `[super class]`时,会转化成 `objc_msgSendSuper` 函数.
 
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数是 objc_super 这样一个结构体,其定义如下
 struct objc_super {
 __unsafe_unretained id receiver;
 __unsafe_unretained Class super_class;
 };
 
第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self
第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son
 
objc Runtime 开源代码对- (Class)class方法的实现
-(Class)class { return object_getClass(self); 
}

Runtime 模块博文推荐 (❤️数量较多)


作者 Runtime 模块推荐阅读博文
西木 完整总结 http://www.jianshu.com/p/6b905584f536
天口三水羊 objc_msgSend http://www.jianshu.com/p/9e1bc8d890f9
夜千寻墨 详解 http://www.jianshu.com/p/46dd81402f63
袁峥Seemygo 快速上手 http://www.jianshu.com/p/e071206103a4
郑钦洪_ 实现自动化归档 http://www.jianshu.com/p/bd24c3f3cd0a
HenryCheng 消息机制 http://www.jianshu.com/p/f6300eb3ec3d
卖报的小画家Sure Method Swizzling开发实例汇总 http://www.jianshu.com/p/f6dad8e1b848
滕大鸟 OC最实用的runtime总结 http://www.jianshu.com/p/ab966e8a82e2
黑花白花 Runtime在实际开发中的应用 http://www.jianshu.com/p/851b21870d91

Runtime & Runloop 面试最常问到的题整理【建议看】

说明:此面试题针对性的摘录整理,只为方便 在面试路上准备的你 ,会注有原文。


1、整理原文:2017年5月iOS招人心得(附面试题)

Runtime
  1. objc在向一个对象发送消息时,发生了什么?
  2. 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
  3. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
  4. runtime如何实现weak变量的自动置nil?
  5. 给类添加一个属性后,在类结构体里哪些元素会发生变化?
RunLoop
  1. runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
  2. runloop的mode是用来做什么的?有几种mode
  3. 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
  4. 苹果是如何实现Autorelease Pool的?

//-------------------- 【我是分割线】 ---------------------//

整理原文:2017年iOS面试题总结,附上答案

Runtime
01
问题: objc在向一个对象发送消息时,发生了什么?
解答: 根据对象的 isa 指针找到类对象 id,在查询类对象里面的 methodLists 方法函数列表,如果没有在好到,在沿着 superClass ,寻找父类,再在父类 methodLists 方法列表里面查询,最终找到 SEL ,根据 id 和 SEL 确认 IMP(指针函数),在发送消息;
03
问题: 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
解答: 当发送消息的时候,我们会根据类里面的 methodLists 列表去查询我们要动用的SEL,当查询不到的时候,我们会一直沿着父类查询,当最终查询不到的时候我们会报 unrecognized selector 错误,当系统查询不到方法的时候,会调用 +(BOOL)resolveInstanceMethod:(SEL)sel 动态解释的方法来给我一次机会来添加,调用不到的方法。或者我们可以再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法来告诉系统,该调用什么方法,一来保证不会崩溃。
04
问题: 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
解答: 1、不能向编译后得到的类增加实例变量 2、能向运行时创建的类中添加实例变量。【解释】:1. 编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理strong weak 引用.所以不能向存在的类中添加实例变量。2. 运行时创建的类是可以添加实例变量,调用class_addIvar函数. 但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
05
问题: runtime如何实现weak变量的自动置nil?
解答: runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
06
问题: 给类添加一个属性后,在类结构体里哪些元素会发生变化?
解答: instance_size :实例的内存大小;objc_ivar_list *ivars:属性列表
RunLoop
01
问题: runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
解答: runloop: 从字面意思看:运行循环、跑圈,其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)事件。runloop和线程的关系:一个线程对应一个RunLoop,主线程的RunLoop默认创建并启动,子线程的RunLoop需手动创建且手动启动(调用run方法)。RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop。
02
问题: runloop的mode是用来做什么的?有几种mode?
解答: model:是runloop里面的运行模式,不同的模式下的runloop处理的事件和消息有一定的差别。系统默认注册了5个Mode:(1)kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。(2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。(3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。(4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。(5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。注意iOS 对以上5中model进行了封装 NSDefaultRunLoopMode、NSRunLoopCommonModes
03
问题: 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
解答: nstime对象是在 NSDefaultRunLoopMode下面调用消息的,但是当我们滑动scrollview的时候,NSDefaultRunLoopMode模式就自动切换到UITrackingRunLoopMode模式下面,却不可以继续响应nstime发送的消息。所以如果想在滑动scrollview的情况下面还调用nstime的消息,我们可以把nsrunloop的模式更改为NSRunLoopCommonModes.
04
问题: 苹果是如何实现Autorelease Pool的?
解答: Autorelease Pool作用:缓存池,可以避免我们经常写relase的一种方式。其实就是延迟release,将创建的对象,添加到最近的autoreleasePool中,等到autoreleasePool作用域结束的时候,会将里面所有的对象的引用计数器 - autorelease.

Reading

原文作者:白开水ln
链接:https://www.jianshu.com/p/19f280afcb24
(不过文章找不到了,希望我保存下来的能帮助到你)

这是我在博客上找到的:https://blog.csdn.net/x32sky/article/details/79486128

你可能感兴趣的:(iOS 模块详解—「Runtime」)