runtime原理及应用

2019-03-24  本文已影响0人  任梦RM

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");

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

==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);
}
上一篇下一篇

猜你喜欢

热点阅读