iOS Runtime

​ 前言:OC是一门动态性比较强的语言,它的动态性就是由Runtime支撑和实现的。本文先介绍了Runtime的概念,然后详细地介绍了OC的消息转发机制,最后介绍了几种Runtime常见的使用场景,并配上具体的代码说明。

一、Runtime概念:

1、Runtime概念:

​ Runtime是一套C语言的API,封装了很多动态性相关的函数。OC是一门动态性比较强的语言,允许很多操作推迟到程序运行时再进行,OC的动态性就是由Runtime支撑和实现的,平时编写的OC代码,底层都是转换成Runtime API进行调用。

2、OC的消息机制:

​ OC中的方法调用其实都是转化成了objc_msgSend函数调用,给receiver(方法调用者)发送一条消息(selector方法名),objc_msgSend有三个阶段:消息发送阶段(当前类、父类中查找)、动态方法解析阶段、消息转发阶段。

二、探寻消息转发机制:

1、OC中方法调用过程:

​ 如果需要了解OC对象的内部结构,请点击OC对象的本质

​ 一个普通的对象方法[objc method],编译时转成消息发送objc_msgSend(objc, method),Runtime执行时流程如下:

​ 1)通过objc的ISA指针找到它的class;

​ 2)在它的class里methods找到method方法;

​ 3)如果它的class没有该method,那么就去父类中查找;

​ 4)一旦找到该method,那么就执行它的实现IMP;

​ 5)如果在父类中还找不到,那么进入动态方法解析+(BOOL)resolveInstanceMethod:(SEL)sel(经测试,如果直接返回YES或者NO都会执行后续的forward方法);

​ 6)如果动态解析没有添加方法,那么进入到消息转发阶段-(id)forwardingTargetForSelector:(SEL)aSelector;

​ 7)如果消息转发的对象还找不到方法,就抛出经典异常unrecognized selector,找不到该方法;

​ 在第一步objc通过ISA指针找到它的class之后,严格来说先从它的class中的缓存中查询方法method,如果没有则继续在methods方法列表进行查询,后续操作一致。

代码验证如下:创建一个Dog类

@interface Dog : NSObject
- (void)wangWang; //正常调用
- (void)wangWangResolve; //方法解析
- (void)wangWangForward_YES; //消息转发YES
- (void)wangWangForward_NO; //消息转发NO
- (void)wangWangForward_All; //完整的消息转发
@end

// .m文件
#import "Dog.h"
#import "Cat.h"
#import <objc/runtime.h>
@implementation Dog
// 直接实现方法
- (void)wangWang {
    NSLog(@"dog wangWang");
}

// 动态解析实例方法
//+ (BOOL)resolveClassMethod:(SEL)sel //解析类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(wangWangResolve))
    {
          class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// C语言函数
void dynamicMethodIMP(id self, SEL _cmd)
{
    NSLog(@"dog wangWangResolve");
}

// 转发备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(wangWangForward_YES)) {
        return [Cat new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

//签名,进入forwardInvocation
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(wangWangForward_All)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"]; 
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;

    Cat *cat = [Cat new];
    if([cat respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:cat];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}
@end

创建一个Cat类:

@interface Cat : NSObject
- (void)wangWangForward_YES; //消息转发YES
- (void)wangWangForward_All; //完整的消息转发
@end

// .m文件
@implementation Cat
- (void)wangWangForward_YES {
    NSLog(@"cat wangWangForward_YES");
}

- (void)wangWangForward_All {
    NSLog(@"cat wangWangForward_All");
}
@end

分别运行注释代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Dog *dog = [Dog new];
    [dog wangWang];
//    [dog wangWangResolve];
//    [dog wangWangForward_YES];
//    [dog wangWangForward_NO];
//      [dog wangWangForward_All];
}

打印结果:

// [dog wangWang] 正常在.m文件实现方法
dog wangWang

// [dog wangWangResolve] 动态解析添加方法
dog wangWangResolve

// [dog wangWangForward_YES] 成功消息转发,对象是cat
cat wangWangForward_YES

// [dog wangWangForward_NO] 前面阶段都找不到方法,抛出异常
-[Dog wangWangForward_NO]: unrecognized selector sent to instance 0x6000031ec350

// [dog wangWangForward_All] 完整的消息转发
cat wangWangForward_All
2、完整的消息转发:

​ 如果forwardingTargetForSelector这一步还不能处理消息,那唯一能做的就是启用完整的消息转发机制了。首先发送一个methodSignatureForSelector消息,如果有签名返回,则进入到forwardInvocation,在该方法里进行最后的消息转发,否则直接发送doesNotRecognizeSelector消息,程序抛出异常。

至于“v@:”具体的意思,可以查看官方文档Type Encodings

三、Runtime应用:

​ Runtime应用场景非常多,这里介绍一些项目中常用的场景:

​ 1)关联对象(AssociateObject)给分类添加的属性实现setter和getter方法,运行中方法替换(method_exchangeIMP),传送门-Category分类;

​ 2)KVO的底层实现,通过runtime动态创建一个派生类对象,传送门-KVC & KVO;

​ 3)实现字典和模型的自动转换(MJExtension),通过runtime遍历Model的所有属性,把服务器返回的JSON格式转成字典,然后通过KVC给Model赋值;

​ 4)实现NSCoding的自动归档和解档,通过runtime遍历Model的所有属性,并对属性进行encode和decode操作;

​ 5)其它runtime的API调用;

1、字典和模型的自动转换:

​ 对NSObject写一个分类,添加一个initWithDict:(NSDictionary)dict分类,如下:

@interface NSObject (DictToModel)
- (instancetype)initWithDict:(NSDictionary *)dict;
@end

// .m
@implementation NSObject (DictToModel)

- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [self init]) {
        // 获取类的属性及属性对应的类型
        NSMutableArray *keys = [NSMutableArray array];
        NSMutableArray *attributes = [NSMutableArray array];
        
        unsigned int outCount;
        objc_property_t *properties = class_copyPropertyList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[i];
            //通过property_getName函数获得属性的名字
            NSString *propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //通过property_getAttributes函数可以获得属性的名字和@encode编码
            NSString *propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        // 立即释放properties指向的内存
        free(properties);

        // 根据类型给属性赋值
        for (NSString *key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;
}

@end

创建一个Person类:

@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) NSInteger age;
@end

运行代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSDictionary *dict = @{@"name": @"路飞",
                           @"age": @(15)
                            };
    Person *p = [[Person alloc] initWithDict:dict];
    NSLog(@"name = %@, age = %ld", p.name, p.age);
}

打印结果:

name = 路飞, age = 15
2、NSCoding的自动归档和解档:

​ 在Model的基类中重写方法:

@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) NSInteger age;
@end

// .m文件
@implementation Person

- (instancetype)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([Person class], &count);
        for (int i = 0; i<count; i++) {
            Ivar ivar = ivars[i]; //拿到Ivar
            const char *name = ivar_getName(ivar); //获取到属性的C字符串名称
            NSString *key = [NSString stringWithUTF8String:name];
            //解档
            id value = [coder decodeObjectForKey:key];
            // 利用KVC赋值
            [self setValue:value forKey:key];
        }
        free(ivars);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    //告诉系统归档的属性是哪些
    unsigned int count = 0; //表示对象的属性个数
    Ivar *ivars = class_copyIvarList([Person class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *name = ivar_getName(ivar);
        NSString *key = [NSString stringWithUTF8String:name];
        //归档 -- 利用KVC
        [coder encodeObject:[self valueForKey:key] forKey:key];
    }
    free(ivars);//在OC中使用了Copy、Creat、New类型的函数,需要释放指针!!(注:ARC管不了C函数)
}

@end

运行代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSDictionary *dict = @{@"name": @"路飞",
                           @"age": @(15)
                            };
    Person *p = [[Person alloc] initWithDict:dict];
    
    NSString *temp = NSTemporaryDirectory();
    NSString *filePath = [temp stringByAppendingPathComponent:@"archive.text"]; //注:保存文件的扩展名可以任意取,不影响。
    //NSLog(@"%@", filePath);
    // 归档
    [NSKeyedArchiver archiveRootObject:p toFile:filePath];
    
    // 解档
    Person *p1 = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
    NSLog(@"name = %@, age = %ld", p1.name, p1.age);
}

打印结果:

name = 路飞, age = 15
3、其它runtime的API调用:

​ 1)[super class]的底层调用,都是通过object_getClass(self)进行调用,代码如下:

​ PS:正常情况下的super与self比较,super是从父类开始查找方法,self是从本类开始;

// 创建一个Person类
@interface Person : NSObject
@end

@implementation Person
@end

// 创建一个Student类继承自Person
@interface Student : Person

@end

@implementation Student

- (id)init {
    if (self = [super init]) {
        Class cls1 = [self class]; //调用object_getClass(self)
        Class cls2 = [self superclass]; 
        Class cls3 = [super class]; //给当前接受者发送消息,当前接受者就是self
        Class cls4 = [super superclass]; 
        NSLog(@"cls1 = %@, cls2 = %@, cls3 = %@, cls4 = %@", cls1, cls2, cls3, cls4);
    }
    return self;
}

@end

运行代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Student *stu = [Student new];
}

打印结果:

cls1 = Student, cls2 = Person, cls3 = Student, cls4 = Person

objc4源码中的NSObject.mm文件中,class的实现:

- (Class)class {
    return object_getClass(self);
}

- (Class)superclass {
    return [self class]->superclass;
}

​ 2)isMemberOfClass与isKindOfClass的区别:

​ -(BOOL)isMemberOfClass:(Class)cls:调用者是否是cls本类的实例;

​ -(BOOL)isKindOfClass:(Class)cls:调用者是否是cls本类的实例或者子类的实例;

​ 查看objc4源码,很容易理解:

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls; //是否是本类
}

- (BOOL)isKindOfClass:(Class)cls {
        // self是否是本类或者子类
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

代码验证,运行代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Student *stu = [Student new];
    BOOL isMember = [stu isMemberOfClass:[Person class]];
    BOOL isKind = [stu isKindOfClass:[Person class]];
    BOOL instance_isMember = [stu isMemberOfClass:[Student class]];
    BOOL class_isMember = [Student isMemberOfClass:[Student class]];
    NSLog(@"isMember = %d, isKind = %d", isMember, isKind);
    NSLog(@"instance_isMember = %d, class_isMember = %d", instance_isMember, class_isMember);
}

打印结果:

isMember = 0, isKind = 1
// [Student isMemberOfClass:[Student class]] 表示Student的元类与Student类比较,结果是false
instance_isMember = 1, class_isMember = 0 

觉得写的不错,有些启发或帮助,点个赞哦!

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

推荐阅读更多精彩内容