Runtime简介

Runtime 概念

runtime(简称运行时),是一套纯C(C和汇编写的) 的API。而 OC 就是运行时机制(消息机制)。
在编译阶段,OC 调用并未实现的函数,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而C语言,函数的调用在编译的时候会决定调用哪个函数,调用未实现的函数就会报错

runtime 消息机制

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

  1. 例子:
创建一个macos工程,就在main.m里写下面简单的代码
Dog *dog = [[Dog alloc] init];
[dog run];
1. 导入 #import ,因为这个里面包含下面两个
#include 
#include 
2.去到 build setting -> 搜索msg ->将Enable Strict Checking of objc_msgSend Calls 改为no 
否则使用 objc_msgSend 编译出错,因为xcode默认不建议使用
3.去到main.m所在的目录,在终端用下面命令编译一下
clang -rewrite-objc main.m
就会生成一个main.cpp文件
4.打开该文件看最下面main方法,可以看到编译后的代码就是runtime
  1. 使用:
    objc_msgSend(id self, SEL op, ...)
    参数:oc对象,方法编号,其他参数...
Dog *dog = [[Dog alloc] init];
[dog run];
可以写成下面的
//Class 类类型  就是一个特殊的对象
Dog *dog = objc_msgSend([Dog class], @selector(alloc));
dog = objc_msgSend(dog, @selector(init));
objc_msgSend(dog, @selector(run));
//
// 底层的实际写法
Dog *dog = objc_msgSend(objc_getClass("Dog"),sel_registerName("alloc"));
dog = objc_msgSend(dog, sel_registerName("init"));
objc_msgSend(dog, @selector(run));
  1. 消息机制方法调用流程
    对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
    OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中根据方法编号(SEL)去查找对应方法,找到只是最终函数实现地址(IMP),根据地址去方法区调用对应函数。
    补充:每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。
runtime 使用场景
  1. 动态交换两个方法的实现(method swizzling)HOOK思想
    需求:给系统的imageNamed添加额外功能(是否加载图片成功)
    方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
    方案二:搞个分类,定义一个能加载图片并且能打印的方法(弊端:不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super,所以要 自己实现一个带有扩展功能的方法.但这样就得改调用的方法,改动大)
    runtime方式实现步骤:
    1.给UIImageView添加分类
    2.自定义并实现带有扩展功能的方法
    3.交换方法
- (void)viewDidLoad {
    [super viewDidLoad];
    UIImage *image = [UIImage imageNamed:@"123"];
}

#import 
@implementation UIImage (Image)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    // 获取方法地址
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
    // 交换方法地址
    if (!class_addMethod([self class], @selector(ln_imageNamed:), method_getImplementation(ln_imageNamedMethod), method_getTypeEncoding(ln_imageNamedMethod))) {
        method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
    }
    });
}
// 自己定义的方法
+ (UIImage *)ln_imageNamed:(NSString *)name {
    UIImage *image = [UIImage ln_imageNamed:name];
    if (image) {
        NSLog(@"load image success");
    } else {
        NSLog(@"load image failed");
    }
    return image;
}
@end

上面代码执行过程,会先执行load方法,这个时候imageNamed:和ln_imageNamed:就交换了,走到viewDidLoad的 [UIImage imageNamed:@"123"] 时,实际上执行的是ln_imageNamed:,ln_imageNamed:里面又调用ln_imageNamed:,实际上调用的是imageNamed:,这样就根据imageNamed:的返回值来判断。

Runtime简介_第1张图片
屏幕快照 2017-08-10 上午12.43.14.png

说明以及注意事项:

  • 方法交换为什么写在load方法
    load 把类加载进内存的时候调用,只会调用一次
  • 为了避免Swizzling的代码被重复执行(调用[super load]),利用dispatch_once函数内代码只会执行一次的特性。
  • class_getClassMethod(获取某个类的方法)
    class_getInstanceMethod (获取某个对象的方法)
  • IMP本质上就是函数指针,所以我们可以通过打印函数地址的方式,查看SEL和IMP的交换流程
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
NSLog(@"%p", method_getImplementation(imageNamedMethod));
NSLog(@"%p", method_getImplementation(ln_imageNamedMethod));
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
  • 使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。而且self没有交换的方法实现,但是父类有这个方法(或者自己有这个方法),这样就会调用父类的方法,结果就不是我们想要的结果了。所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了
  1. runtime结合kvc实现NSCoding的自动归档和解档
    如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,就非常麻烦。
  • 原来的做法
遵守协议NSCoding
@property (nonatomic, copy) NSString *name;
- (void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:_Name forKey:@"name"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        self.movieName = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}
  • 新做法(主要代码)
//解档
- (void)decode:(NSCoder *)aDecoder {
    // 一层层父类往上查找,对父类的属性执行归解档方法
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 如果有实现该方法再去调用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }
            
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
    }
    
}
// 归档
- (void)encode:(NSCoder *)aCoder {
    // 一层层父类往上查找,对父类的属性执行归解档方法
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            // 获取成员变量的名字
            const char *name = ivar_getName(ivar);
            //// C字符串 -> OC字符串
            NSString *key = [NSString stringWithUTF8String:name];
            
            // 如果有实现该方法再去调用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) continue;
            }
            
            id value = [self valueForKeyPath:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
    }
}
  1. 动态添加方法
    如果一个类方法非常多,因为需要给每个方法生成映射表,实际上只要一个类实现了某个方法,就会被加载进内存,加载类到内存的时候就比较耗费资源。当硬件内存过小的时候,如果我们将每个方法都直接加到内存当中去,但是很久都不用一次,这样就造成了浪费,那如果我想像懒加载一样,先把方法定义好,但是只有当你用的时候我再加载你,这就需要动态添加了。
    当performSelector方法调用某个sel的时候,这时候会到调用对象的+ (BOOL)resolveInstanceMethod:(SEL)sel方法中,如果这里返回是NO,就表示找不到。
  • 看下面的例子
// 动态添加方法就不会报错
    Person * p = [[Person alloc] init];
    [p performSelector:@selector(eat:) withObject:@"吃过了"];

//下面代码在Person.m里
#import 
void addEat(id self, SEL _cmd, NSString *str) {
    NSLog(@"%@", str);
}
// 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"eat:")) {
        BOOL isSuccess = class_addMethod(self, sel, (IMP)addEat, "v@:@");
        return isSuccess;
    }
    return [super resolveInstanceMethod:sel];
}
  • class_addMethod参数解释(可以command+shift+0查看官方文档)
    class_addMethod(Class cls, SEL name, IMP imp,const char *types)
  1. class: 给哪个类添加方法
  2. SEL: 添加方法的方法编号
  3. IMP: 方法实现 (添加方法的函数实现(函数地址))
  4. type: 方法类型,(返回值+参数类型)
    (1) v 返回值类型是void
    (2)@ 对象->self
    (3): 表示SEL->_cmd
    (4)@ 第四个参数
  • resolveInstanceMethod的作用
    当调用了没有实现的方法没有实现就会调用,然后就可以根据他的参数sel(参数sel就是没有实现的方法)来做一系列的操作。

4.给分类添加属性
在分类中,所写的@property (nonatomic, strong) NSString *name;都仅仅是生成了get和set方法,并没有生成对应的_name属性,但是有时候我们会有一种需求,想要让分类中保存一下新的属性值,因为set和get方法只能是对已经有的东西做操作,比如说最常用的UIView的分类我们对frame中的x,y,width,height做操作。

//给Person添加一个分类addProperty
//在Person+addProperty.h中
@property (nonatomic, strong) NSString *name;
//在Person+addProperty.m中
#import 
@implementation Person (addProperty)
- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
    return objc_getAssociatedObject(self, @"name"); 
}

- (void)viewDidLoad {
    [super viewDidLoad];
    //给分类动态添加属性
    Person * p1 = [[Person alloc] init];
    p1.name = @"这是给分类添加的属性";
    NSLog(@"%@",p1.name);
}

解释:
objc_setAssociatedObject方法

/**
     *  根据某个对象,还有key,还有对应的策略(copy,strong等) 动态的将值设置到这个对象的key上
     *  @param object 某个对象
     *  @param key    属性名,根据key去获取关联的对象
     *  @param value  要设置的值
     *  @param policy 策略(copy,strong,assign等)
     */
    OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

objc_getAssociatedObject方法

/**
     *  根据某个对象,还有key 动态的获取到这个对象的key对应的属性的值
     *  @param object 某个对象
     *  @param key    key
     *  @return 对象的值
     */
    OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

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

步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
MJExtension 字典转模型实现,底层也是对 runtime 的封装。

注:本文参考 http://www.jianshu.com/p/19f280afcb24
更全面的例子参考 https://github.com/lizelu/ObjCRuntimeDemo

你可能感兴趣的:(Runtime简介)