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 <objc/message.h>,因为这个里面包含下面两个
#include <objc/objc.h>
#include <objc/runtime.h>
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 <objc/message.h>
@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:的返回值来判断。

屏幕快照 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 <objc/runtime.h>
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 <objc/message.h>
@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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,697评论 7 64
  • 一、Runtime简介 RunTime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消...
    窦豆逗阅读 156评论 0 0
  • Runtime简介以及常见的使用场景 Runtime简称运行时,是一套比较底层的纯C语言的API,作为OC的核心...
    轻云_阅读 1,169评论 5 23
  • RunTime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。对于C语言,函数...
    _心暖阅读 512评论 1 1
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9