runtime原理及应用

runtime简介

Runtime就是运行时, 核心就是消息机制. 对OC的函数调用,是一个动态调用过程,只有在运行的时候runtime系统才能知道真正调用的哪一个函数(C语言在函数调用过程中, 编译时候就已经决定会调用哪个函数了).

iOS Runtime中实例对象和类的本质

实例对象的本质

OC是一门面向对象的编程语言,在编译过程中,编译器会将OC对象转化成结构体.
在objc.h中找到:

typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

可以看到我们常用的Class, id等关键字的定义.
OC中实际的类Class, 会被编译成struct objc_class. 我们操作的类的对象实例是struct objc_object, 并且该结构体中有一个指针指向struct objc_class.

iOS OC中类的本质

OC对象的结构体中有一个Class指针能够理解, 因为要知道该对象是哪个类的对象.但是我们在objc-runtime-new.h中发现objc_class继承自objc_object的.

struct objc_class : objc_object {
    // Class ISA; // 继承了
    Class superclass;
    ...

在runtime.h中, 我们看到OC类的结构体struct objc_class的具体定义

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

OC中类Class中也有一个指针指向Class, 因此类Class本质上也是一个对象, 我们一般称为类对象, 这个指向的Class是就是元类(metaClass)的对象.

当我们调用对象方法时候, 会通过对象中的Class指针找到对应的Class,然后调用实例方法,同理当我们调用类方法时候, 会通过Class中的Class指针找到对应的meta Class,然后调用meta Class中的方法.

OC一般会隐藏元类, 并且元类也是某个类的实例, 这个类我们一般称为根元类(root meta Class). 并且所有的元类的根元类都是一个, 并且根元类的元类是它自己. (实际中根元类是NSObject的元类)

NSString实例的isa指针链:


image
iOS 中OC方法调用的本质

OC中的方法调用称为消息发送, 具体格式是[receiver message].例如:

NSMutableString *str = [[NSMutableString alloc] initWithString:@"hello"];
[str appendString:@" world"];

其中str就是receiver, appendString:就是message.

在message.h头文件中如下方法,这个方法是runtime的核心方法,

void objc_msgSend(void /* id self, SEL op, ... */ )
objc_msgSend(receiever, selector, arg1, arg2, ...)

调用实例如下:

objc_msgSend(str, @selector(appendString:), @" world");

该消息方法为消息的动态绑定完成了以下工作:

  • 它会主动查找receiver的selector对应的方法实现IMP
  • 然后将参数传递给receiver object, 然后调用这IMP
  • 最后返回该方法的返回值

==IMP:一个函数指针,保存了方法的地址==

为了使得objc_msgSend能完成通过selector查找receiver对应的IMP, 我们知道一个OC类和OC对象会有一个isa指针,指向他们各自的Class, 同时OC类还有一个super指针指向父类.

具体过程就是通过isa指针找到对应的class struct, 然后在dispatch table里面查找selector对应的方法, 如果没有找到,那么通过super指针查找父类的dispatch table, 一直找下去, 直到NSObject类, 如果还没有找到,就调用NSObject的doesNotRecognizeSelector:方法, 然后报unrecognized selector错误.

iOS runtime实战应用

1.iOS runtime 进行添加属性,并支持KVO监听

iOS 中category和runtime的AssociatedObject是两大非常重要的工具:

category可以给既有类直接添加方法
associateObject可以给既有类添加属性(类似成员变量)
结合这两个工具, 那么通过category添加property方法.然后结合associateObject增加关联对象,完成属性存取.

==需要加入头文件#import <objc/runtime.h>==

@interface UIViewController (Extension)
@property (nonatomic, copy) NSString * categoryString;
@end

@implementation UIViewController (Extension)
-(NSString *)categoryString{
    return objc_getAssociatedObject(self, @selector(categoryString));
}


-(void)setCategoryString:(NSString *)categoryString{
    objc_setAssociatedObject(self, @selector(categoryString), categoryString, OBJC_ASSOCIATION_COPY);
}

@end

并且这种方法也支持KVO的监听:

-(void)test{
    self.categoryString = @"Runtime生成的属性";
    [self addObserver:self forKeyPath:@"categoryString" options:NSKeyValueObservingOptionNew context:nil];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"<接收到通知: object:%@ keyPath:%@ change:%@>", object, keyPath,change);
}

2.(1)交换两个方法的实现,(2)拦截系统自带的方法调用功能

Method class_getClassMethod(Class cls , SEL name) //获得某个类的类方法

Method class_getInstanceMethod(Class cls , SEL name)//获得某个类的实例对象方法

void method_exchangeImplementations(Method m1 , Method m2)//交换两个方法的实现
案例1:方法简单的交换

创建一个Person类,类中实现以下两个类方法,并在.h 文件中声明

+ (void)game {
    NSLog(@"游戏");
}

+ (void)study {
    NSLog(@"学习");
}
[Person study];
[Person game];

下面通过runtime 实现方法交换,类方法用class_getClassMethod ,对象方法用class_getInstanceMethod

// 获取两个类的类方法
Method m1 = class_getClassMethod([Person class], @selector(run));
Method m2 = class_getClassMethod([Person class], @selector(study));
// 开始交换方法实现
method_exchangeImplementations(m1, m2);
// 交换后
[Person study];
[Person game];

控制台打印

2018-04-26 15:20:37.224745+0800 runtime-test[15020:7430831] 学习
2018-04-26 15:20:37.224835+0800 runtime-test[15020:7430831] 游戏
2018-04-26 15:20:37.225668+0800 runtime-test[15020:7430831] 游戏
2018-04-26 15:20:37.225720+0800 runtime-test[15020:7430831] 学习
案例2:拦截系统方法

1、为UIImage建一个分类(UIImage+Category)
2、在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,添加自己的逻辑判断

+ (UIImage *)xxx_imageNamed:(NSString *)name {
    double version = 11.11;
    if (version == 11.11) {
        name = [name stringByAppendingString:@"5.1_time"];
    }
    return [UIImage xxx_imageNamed:name];
}

3、分类中重写UIImage的load方法,实现方法的交换(只要能让其执行一次方法交换语句,load再合适不过了)

+ (void)load {
    // 获取两个类的类方法
    Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
    Method m2 = class_getClassMethod([UIImage class], @selector(xxx_imageNamed:));
    // 开始交换方法实现
    method_exchangeImplementations(m1, m2);
}

==注意:自定义方法中最后一定要再调用一下系统的方法,让其有加载图片的功能,但是由于方法交换,系统的方法名已经变成了我们自定义的方法名(就是用我们的名字能调用系统的方法,用系统的名字能调用我们的方法),这就实现了系统方法的拦截!==

3.获得一个类的成员变量( Ivar )、属性( Property )、方法( Method )、协议( Protocol )

获得某个类的所有成员变量(outCount 会返回成员变量的总数)

参数:
1、哪个类
2、放一个接收值的地址,用来存放属性的个数
3、返回值:存放所有获取到的属性,通过下面两个方法可以调出名字和类型

Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
获得成员变量的名字
const char *ivar_getName(Ivar v)
获得成员变量的类型
const char *ivar_getTypeEndcoding(Ivar v)
案例1:获取Person类中所有成员变量的名字和类型
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &outCount);

// 遍历所有成员变量
for (int i = 0; i < outCount; i++) {
    // 取出i位置对应的成员变量
    Ivar ivar = ivars[i];
    const char *name = ivar_getName(ivar);
    const char *type = ivar_getTypeEncoding(ivar);
    NSLog(@"成员变量名:%s 成员变量类型:%s",name,type);
}
// 注意释放内存!
free(ivars);

==同样:==

// 测试 打印属性列表
- (void)testPrintPropertyList {
    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]);
    }
    
    free(propertyList);
}
// 测试 打印方法列表
- (void)testPrintMethodList {
    unsigned int count;
    
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
        Method method = methodList[i];
        NSLog(@"method----="">%@", NSStringFromSelector(method_getName(method)));
    }
    
    free(methodList);
}
// 测试 打印协议列表
- (void)testPrintProtocolList {
    unsigned int count;
    
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol----="">%@", [NSString stringWithUTF8String:protocolName]);
    }
    
    free(protocolList);
}
案例2:利用Runtime进行 json/dict -> model
-(instancetype)initWithNSDictionary:(NSDictionary *)dict{
    self = [super init];
    if (self) {
        [self processDict:dict];
    }
    return self;
}

-(void)processDict:(NSDictionary *)dict{
    NSMutableArray *keys = [[NSMutableArray alloc] init];
    unsigned int count = 0;
    objc_property_t *props = class_copyPropertyList([self class], &count);
    for (int i = 0; i < count; i++) {
        objc_property_t prop = props[i];
        const char *propCStr = property_getName(prop);
        NSString *propName = [NSString stringWithCString:propCStr encoding:NSUTF8StringEncoding];
        [keys addObject:propName];
    }
    free(props);
    for (NSString *key in keys) {
        if ([dict valueForKey:key]) {
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
}
案例3:利用runtime 获取所有属性来重写归档解档方法
// 解档方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    // 获取所有成员变量
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self class], &outCount);
    
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        // 将每个成员变量名转换为NSString对象类型
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 忽略不需要解档的属性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }
        
        // 根据变量名解档取值,无论是什么类型
        id value = [aDecoder decodeObjectForKey:key];
        // 取出的值再设置给属性
        [self setValue:value forKey:key];
        // 这两步就相当于以前的 self.age = [aDecoder decodeObjectForKey:@"_age"];
    }
    free(ivars);
    return self;
}

// 归档调用方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
    // 获取所有成员变量
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        // 将每个成员变量名转换为NSString对象类型
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 忽略不需要归档的属性
        if ([[self ignoredNames] containsObject:key]) {
            continue;
        }
        
        // 通过成员变量名,取出成员变量的值
        id value = [self valueForKeyPath:key];
        // 再将值归档
        [aCoder encodeObject:value forKey:key];
        // 这两步就相当于 [aCoder encodeObject:@(self.age) forKey:@"_age"];
    }
    free(ivars);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 周末没事,我也休息一下。出去跟朋友们聚会交流,很长时间没有出去了。他们都在问我天天忙什么?我说在家写写字,...
    Vultr阅读 509评论 0 0
  • 每一个出口,都成了束缚。 烟的舞,看似散漫,谁又知道他不是在挣扎点什么。 写几个字,觉得短了,就又要写到长点,觉得...
    写一个世界的阅读 204评论 0 0