iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.

栏目: IOS · 发布时间: 7年前

内容简介:iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.

引导

对于从事 iOS 开发人员来说,所有的人都会答出「 runtime 是运行时 」,什么情况下用 runtime ?,大部分人能说出「 给分类动态添加属性 || 交换方法 」,再问一句「 runtime 消息机制的调用流程 || 能体现runtime 强大之处的应用场景 」,到这,能知道答案的寥寥无几,很少有人会说到 “黑魔法” 这三个字,

runtime是 iOS 编程中比较难的模块,想要深入学习 OC,那 runtime 是你必须要熟练掌握的东西,下面是我对 runtime 的整理,从零开始,由浅入深,并且带了几个 runtime 实际开发的应用场景。

不管谁的博客上面写的文章(也包括自己),阅读的你要敢于去验证,停止无意义的⏹copy :two_men_holding_hands: paste。

在「时间和知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,以提高文章质量。

iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.

目录:

  1. runtime 概念
  2. runtime 消息机制
  3. runtime 方法调用流程「消息机制」
  4. runtime 运行时常见作用
  5. runtime 常用开发应用场景「工作掌握」
    1.runtime 交换方法
    2.runtime 给分类动态添加属性
    3.runtime 字典转模型(Runtime 考虑三种情况实现)
  6. runtime 运行时其它作用「面试熟悉」
    1.动态添加方法
    2.动态变量控制
    3.实现NSCoding的自动归档和解档
    4.runtime 下Class的各项操作
    5.runtime 几个参数概念
  7. 什么是 method swizzling(俗称黑魔法)
  8. 最后一道面试题的注解
  9. runtime模块简友文章推荐(:heart:数量较多)
  10. 实战应用场景(持续)
  11. 期待 & 后续

本篇文章较长一些,强烈建议先 :heart: 收藏,在进行阅读 !

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 方法。

iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
OC-->runtime

简单示例:

验证:方法调用,是否真的是转换为消息机制?

  • 必须要导入头文件 #import <objc/message.h>

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

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

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

    • 注解4:这里一般不会直接导入 <objc/runtime.h>

      iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
      message.h
  • 示例代码: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));

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 <objc/message.h>
@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


// 打印输出
2017-02-17 17:52:14.693 runtime[12761:543574] runtime添加额外功能--加载成功

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

runtime 给分类动态添加属性

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

应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。

注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了 @property ,但是仅仅会自动生成 getset 方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过 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);

// 打印输出
2017-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123

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

runtime 字典转模型

字典转模型的方式:

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

  • 字典转模型 KVC 实现

    • KVC 字典转模型弊端:必须保证,模型中的属性和字典中的 key 一一对应。
    • 如果不一致,就会调用 [<Status 0x7fa74b545d60> 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 方式实现:

说明:下面这个示例,是考虑三种情况包含在内的转换示例,具体可以看图上的注解

iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
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;
}
iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
runtime字典转模型-->数组中装着模型 打印输出

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

本文为「简书-白开水ln」作者原创;我的写作,希望能简化到初学者尽快入门和老司机繁琐回顾 ^_^.

这里提到的你如果不是很清楚,建议参考我的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

// 打印输出
2017-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<count; i++) {
           Ivar var = ivar[i];
           const char *varName = ivar_getName(var);
           NSString *name = [NSString stringWithUTF8String:varName];
           if ([name isEqualToString:@"_age"]) {
               object_setIvar(self.xiaoMing, var, @"20");
               break;
           }
       }
       NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
    }

实现NSCoding的自动归档和解档

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

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

// Movie.h文件
//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding
@interface Movie : NSObject<NSCoding>

@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 <objc/runtime.h>
@implementation Movie

- (void)encodeWithCoder:(NSCoder *)encoder

{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([Movie class], &count);

    for (int i = 0; i<count; i++) {
        // 取出i位置对应的成员变量
        Ivar ivar = ivars[i];
        // 查看成员变量
        const char *name = ivar_getName(ivar);
        // 归档
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [encoder encodeObject:value forKey:key];
    }
    free(ivars);
}

- (id)initWithCoder:(NSCoder *)decoder
{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([Movie class], &count);
        for (int i = 0; i<count; i++) {
        // 取出i位置对应的成员变量
        Ivar ivar = ivars[i];
        // 查看成员变量
        const char *name = ivar_getName(ivar);
       // 归档
       NSString *key = [NSString stringWithUTF8String:name];
      id value = [decoder decodeObjectForKey:key];
       // 设置到成员变量身上
        [self setValue:value forKey:key];

        }
        free(ivars);
    } 
    return self;
}
@end

这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,代码有点多,

好说下面看看更加简便的方法:两句代码搞定。

#import "Movie.h"
#import <objc/runtime.h>

#define encodeRuntime(A) \
\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [self valueForKey:key];\
[encoder encodeObject:value forKey:key];\
}\
free(ivars);\
\

#define initCoderRuntime(A) \
\
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([A class], &count);\
for (int i = 0; i<count; i++) {\
Ivar ivar = ivars[i];\
const char *name = ivar_getName(ivar);\
NSString *key = [NSString stringWithUTF8String:name];\
id value = [decoder decodeObjectForKey:key];\
[self setValue:value forKey:key];\
}\
free(ivars);\
}\
return self;\
\

@implementation Movie

- (void)encodeWithCoder:(NSCoder *)encoder

{
    encodeRuntime(Movie)
}

- (id)initWithCoder:(NSCoder *)decoder
{
    initCoderRuntime(Movie)
}
@end

优化:上面是 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<count; i++) {
       const char *propertyName = property_getName(propertyList[i]);
       NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
    }
  • 获取方法列表

    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i; i<count; i++) {
       Method method = methodList[i];
       NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
    }
  • 获取成员变量列表

    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
    }
  • 获取协议列表

    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol---->%@", [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
*/

runtime 几个参数概念

以上的几种方法应该算是 runtime 在实际场景中所应用的大部分的情况了,平常的编码中差不多足够用了。

这里在对 runtime 几个参数概念,做一简单说明

1、objc_msgSend

这是个最基本的用于发送消息的函数。

其实编译器会根据情况在 objc_msgSendobjc_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`实现的原理。

上面讲到的所有东西都在Demo里,如果你感觉这样难以理解,那强烈建议你下载Demo ,运行代码加上文字注解,效果会更好,如果你觉得不错,还请为我的Demo star一个。

什么是 method swizzling(俗称黑魔法)

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

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

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

iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
selector --> 对应的IMP
  • 交换方法的几种实现方式
    • 利用 method_exchangeImplementations 交换两个方法的实现
    • 利用 class_replaceMethod 替换方法的实现
    • 利用 method_setImplementation 来直接设置某个方法的 IMP
      iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
      交换方法

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

这里可以参考权威这篇: OC运行时黑魔法 Method Swizzling

iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
 -- 小黑不要走,我一定咬吃了你!

最后一道面试题的注解

下面的代码输出什么?

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

先思考一下,会打印出来什么:question:

关注我的更多干货分享 ^_^.

答案:都输出 Son

  • class 获取当前方法的调用者的类, superClass 获取当前方法的调用者的父类, super 仅仅是一个编译指示器,就是给编译器看的,不是一个指针。
  • 本质:只要编译器看到 super 这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用

这个题目主要是考察关于 objc 中对 selfsuper 的理解:

  • 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模块简友文章推荐(:heart:数量多)

简友 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

runtime效果图

iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
坐下来 品一杯白开水,写的小样在下面 ~

期待

  • 如果在阅读过程中遇到 error,希望你能 Issues 我,谢谢。

  • 如果你想为【本文相关】分享点什么,也希望你能 Issues 我,我非常想为这篇文章增加更多实用的内容,谢谢。

  • 「博客原文」,对本文我会【不定时、持续更新、一些 学习心得与文章、实用才是硬道理】^_^.

About me

【我也是对所花费时间的一个总结】

我只是个【有思想的伐码猿:monkey:】加上【自己的学习总:coffee:️】写出来的文章。


以上所述就是小编给大家介绍的《iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

离散数学及其应用(原书第6版·本科教学版)

离散数学及其应用(原书第6版·本科教学版)

[美] Kenneth H. Rosen / 袁崇义、屈婉玲、张桂芸 / 机械工业出版社 / 2011-11 / 49.00元

《离散数学及其应用》一书是介绍离散数学理论和方法的经典教材,已经成为采用率最高的离散数学教材,仅在美国就被600多所高校用作教材,并获得了极大的成功。第6版在前5版的基础上做了大量的改进,使其成为更有效的教学工具。 本书基于该书第6版进行改编,保留了国内离散数学课程涉及的基本内容,更加适合作为国内高校计算机及相关专业本科生的离散数学课程教材。本书的具体改编情况如下: · 补充了关于范式......一起来看看 《离散数学及其应用(原书第6版·本科教学版)》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器