Runtime入门总结

  1. 简介
  2. Runtime的基础数据结构
  3. 消息发送
    1. 方法调用流程
    2. 动态方法解析
    3. 快速转发
    4. 标准转发
  4. API使用
    1. 动态创建类,添加方法
    2. 分类中动态绑定属性
    3. 字典转模型
    4. 方法交换(method swizzling)

简介


  • runtime是到底是个什么狼狗?
    runtime就是一个由汇编语言和c语言编写的库,它实现了OC语言面向对象和动态语言的特性。多数情况下runtime库都是在幕后工作,但是它也提供了一些API给我们使用。你可以在官方文档查看这些API的使用,也可以在这里下载runtime的开源代码来研究它的具体实现

  • runtime是根据什么原理来实现的?
    答案很简单,就是消息机制。OC中[receiver message]并不是简单的函数调用,它会被编译器转化为[objc_msgSend(receiver, selector)],解释为向object发送一条message消息,程序运行时根据receiver(消息的接受者)和selector来确定执行具体的操作,而不是在编译时决定。

  • clang -rewrite-objc Myclass.m可以查看转换后的代码

Runtime的基础数据结构


objc_msgSend()函数是所有消息发送的必经之路,在我们详细了解消息发送流程之前,先从objc_msgSend()函数入手了解一下Runtime中的数据结构。

objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>)

SEL
objc_msgSend第二个参数为SEL类型,表示方法选择器,在objc.h文件中可以看到其定义:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

实际上它就是一个映射方法的分段字符串。用于指明调用哪个方法,可以理解为区分方法的ID。可以使用@selector()、sel_registerName()或NSSelectorFromString()来获取选择器。

IMP

typedef void (*IMP)(void /* id, SEL, ... */ ); 

它其实就是一个函数指针,指向具体的方法实现,在同一个对象中SELIMP是一一对应的。

id
objc_msgSend的第一个参数,大家对它都不陌生,可以接收OC中任何类型的对象。

typedef struct objc_object *id;

本质上就是一个结构体指针,指向类实例。接着看objc_object

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

包含一个isa指针,指向它所属的类。

Class

typedef struct objc_class *Class;

又是一个指针,指向objc_class,定义在runtime.h

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
}

其中包括指向父类的指针super_class、类的名字name、实例变量的大小instance_size、成员变量列表ivars、方法列表methodLists、缓存cache和协议列表protocols等。其中methodLists就是一个链表,存储所有的实例方法(Method)

typedef struct objc_method *Method;

struct objc_method {
        SEL method_name;
        char *method_types;
                IMP method_imp;
    } method_list[1];

method_types存储着方法的参数类型和返回值累型。
cache用来缓存常用的方法,以达到优化方法查找效率的目的。

struct objc_cache {
    unsigned int mask;            /* total = mask + 1 */
    unsigned int occupied;        
    Method buckets[1];
};

最重要的我们发现Class里也有一个isa指针。

实例对象的isa指针指向实例对象所属的类,那么类的isa指针指向哪里呢?

我们先看个图


image.png
  • 类本身也是一个对象,类对象所属的类称之为元类(MetaClass)
  • 每个对象都是一个类的实例,类中定义了实例方法列表。对象的isa指针指向它所属的类
  • 每个类都是它所属元类的实例,在元类中定义了类方法列表,类的isa指针指向它的元类
  • 每个类都有一个与之相关的元类,所有元类最终都指向根元类(Root meta class),根元类的isa指向自身。
  • 其实根元类的父类就是NSObject,根元类就是NSObject的元类

消息发送


方法调用流程
假如有一个Person类,有一个run的实例方法。实例化一个对象p,[p run]是怎么执行的?

  1. 根据对象p的isa指针,找到所属的类
  2. 根据selector在类的cache缓存中寻找方法实现的地址,找到执行,没找到执行下一步
  3. 在类的methodLists方法列表中寻找,找到执行,没找到执行下一步
  4. 根据类的super_class指针,到父类的缓存中寻找,没找到执行下一步
  5. 在父类的方法列表中寻找,如果没找,继续向上找,一直到NSObject
  6. 如果最终没找到,则会调用resolveInstanceMethod或者resolveClassMethod方法,让我们可以动态添加方法实现

动态方法解析
.h文件

#import <Foundation/Foundation.h>

@interface Person : NSObject

- (void)run;
+ (void)eat:(NSString *)str;

@end

.m文件

#import "Person.h"
#import <objc/runtime.h>

@implementation Person

// 当找不到实例方法实现时,调用此方法。在此方法中我们动态添加一个实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        class_addMethod([self class], sel, imp_implementationWithBlock(^(id self){
            NSLog(@"run");
        }), "v@:");
        
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 找不到类方法时调用,在此动态添加一个类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(eat:)) {
        class_addMethod(object_getClass(self), sel, (IMP)eat, "v@:@");
        // 如果返回NO,则会进入消息转发
        return YES;
    }
    return [super resolveClassMethod:sel];
}

// 函数实现,函数默认都有两个参数
void eat(id self, SEL _cmd, NSString *str) {
    NSLog(@"eat %@",str);
}

@end

需要注意类方法要添加到元类中。

  • [NSObject class]返回类本身
  • [object class]返回对象isa所指的类,简单点说对象是哪个类的实例,就返回哪个类
  • object_getClass返回传入对象的isa所指的类,如果传入的是一个实例,则返回实例所属的类;如果传入的是一个类,则返回类isa指针指向的类,也就是元类
  • v@:@是描述函数的参数类型以及返回值类型的类型编码,v代表函数的返回值为void,第一个@代表第一个参数也就是id self:代表第二个参数SEL _cmd,第二个@代表第三个参数NSString *str。类型编码可参考类型编码

如果以上两个方法返回NO则会调用forwardingTargetForSelector方法进入消息转发

快速转发

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        return [Proxy new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

可以在此将消息转发给其他对象。所以我们需要一个可以相应此消息的对象。新建一个类Proxy,只需在.m文件中给出方法实现。

#import "Proxy.h"

@implementation Proxy

- (void)run{
    NSLog(@"proxy run");
}

@end

这样便完成了快速转发,当Person实例调用run方法时,就会转发到Proxy中,如果Proxy类里有对应的实现,则会执行。
forwardingTargetForSelector中,如果返回nil或者self则会进入标准转发forwardInvocation

标准转发
在调用forwardInvocation之前,会先调用methodSignatureForSelector获取方法签名,方法签名中包含了参数,返回值,以及消息接受者的相关信息。然后包装成一个NSInvocation对象调用forwardInvocation进行最后的消息转发。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        // 构造一个方法签名
        NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        return sig;
    }
    return [super methodSignatureForSelector:aSelector];
}

// 可以将消息转发,可以更改参数值,还可以更改所要调用的方法。总之可以肆无忌惮的做任何事情
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 将方法选择器更改为eat方法
    [anInvocation setSelector:sel_registerName("eat:")];
    NSString *str = @"apple";
    // 添加参数,函数默认有两个参数,所以我们添加参数下标要从2开始
    [anInvocation setArgument:&str atIndex:2];
    // 转发给Proxy 对象
    [anInvocation invokeWithTarget:[Proxy new]];
}

如果在消息标准转阶段不做处理,最后就会抛出unrecognized selector异常,导致程序crash 。
总体来说消息发送的过程可以归纳成下图:

image.png

如果想更加深入了解请看 消息发送与转发机制原理

Runtime API的使用


动态创建类、添加方法、变量

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    // 动态创建一个类
    Class DynaClass = objc_allocateClassPair([NSObject class], "DynaClass", 0);
    
    // 从类中取出一个方法
    Method des = class_getClassMethod([NSObject class], @selector(description));
    // 获取方法的特征,包含参数与返回值的信息
    const char* types = method_getTypeEncoding(des);
    
    // 添加实例方法
    class_addMethod(DynaClass, @selector(objcMethod), (IMP)objcMethod, types);
    
    //添加一个成员变量,只能在objc_registerClassPair之前添加
    class_addIvar(DynaClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    
    // 注册类
    objc_registerClassPair(DynaClass);
    
    // 获取元类
    Class metaCls = objc_getMetaClass("DynaClass");
    // 添加一个类方法
    class_addMethod(metaCls, NSSelectorFromString(@"classMethod"), imp_implementationWithBlock(^(id self, NSString *str){
        NSLog(@"class method %@", str);
    }), "v@:");
    
    
    
    // 根据动态创建的类,实例化一个对象
    id dynaObjc = [[DynaClass alloc] init];
    
    // 访问成员变量
    [dynaObjc setValue:@"动态添加属性" forKey:@"name"];
    NSLog(@"%@", [dynaObjc valueForKey:@"name"]);

    // 调用方法
    NSString *res = [dynaObjc performSelector:@selector(objcMethod)];
    NSLog(@"%@", res);
    
    [DynaClass performSelector:@selector(classMethod) withObject:@"我是参数"];    
}

NSString *objcMethod(id self, SEL _cmd)
{
    return [NSString stringWithFormat:@"hello"];
}

分类中动态绑定属性
分类(Category)本来是不支持添加属性的,即使我们使用@property也只会声明settergetter ,并没有生成对应的实例变量和方法实现。我们可以使用runtime进行动态绑定来达到添加属性的效果,但是实质上只是添加一个关联,并不是真正的添加一个变量到类的地址空间中。

- (void)setTitle:(NSString *)title {
    objc_setAssociatedObject(self, "title", title, OBJC_ASSOCIATION_COPY);
}

- (NSString *)title {
    return objc_getAssociatedObject(self, "title");
}

详细请看关联对象实现原理

字典转模型

  1. 获取model对象的所有属性
  2. 根据属性名字查找字典中的key,取出对应的value
  3. 赋值给model
@implementation NSObject (Model)
+ (id)modelWithDic:(NSDictionary *)dic {
    id pModel = [[self alloc] init];
    
    unsigned int count;
    
    // 获取对象的成员变量数组
    Ivar *iList = class_copyIvarList(self, &count);
    
    for (int i=0; i<count; i++) {
        Ivar var = iList[i];
        // 获取变量的名字
        NSString *varName = [NSString stringWithUTF8String:ivar_getName(var)];
        // 获取变量的类型
        NSString *varType = [NSString stringWithUTF8String:ivar_getTypeEncoding(var)];
        
        // 成员变量都是以下划线开头,所以需要截取一下
        varName = [varName substringFromIndex:1];
        varType = [varType substringWithRange:NSMakeRange(2, varType.length - 3)];
        
        // 根据属性名获取字典的value
        id value = dic[varName];
        
        // 模型嵌套模型。字典的值是字典,需要将其也转换成对应模型
        if ([value isKindOfClass:[NSDictionary class]] && ![varType hasPrefix:@"NS"]) {
            Class class = NSClassFromString(varType);
            value = [class modelWithDic:value];
        }
        
        // 字典的值是数组,数组包含字典,将数组中的字典也转成模型
        if ([value isKindOfClass:[NSArray class]]) {
            // 判断是否实现modelClassInArray协议,协议方法返回数组中字典对应的model类
            if ([self respondsToSelector:@selector(modelClassInArray)]) {
                id idSelf = self;
                NSString *type = [idSelf modelClassInArray][varName];
                Class class = NSClassFromString(type);
                NSMutableArray *modelArray = [[NSMutableArray alloc] init];
                
                for (NSDictionary *dic in value) {
                    id model = [class modelWithDic:dic];
                    [modelArray addObject:model];
                }
                value = modelArray;
            }
        }
        
        if (value) {
            [pModel setValue:value forKey:varName];
        }
    }
    
    free(iList);
    
    return pModel;
}
@end

MJExtensionJSONModel等大部分框架应该也是这种方式实现的。

方法交换
直白点就是调用A的时候,执行的是B的实现,调用B的时候,其实执行的是A。其实就是将两个方法的实现进行交换,如下图:

image.png

举个例子,在执行[NSURL URLWithString:urlStr]这句代码的时候,如果urlStr包含中文,需要先对其进行编码才能正确返回NSURL对象。那么可不可以只修改某一个地方,不用每次调用URLWithString :前都对urlStr进行编码呢?

这时就可以利用方法交换来达到目的。

  1. 新建一个NSURL的分类
  2. 在分类添加一个方法my_ URLWithString :,进行处理中文问题
  3. load方法中将系统的URLWithString :和我们新添的my_ URLWithString :进行交换。
#import "NSURL+category.h"

@implementation NSURL (category)

+ (instancetype)my_URLWithString:(NSString *)URLString
{
    // 此处不会造成死循环,因为my_URLWithString和URLWithString已经交换,
    // 所以调用my_URLWithString实际上就是调用的URLWithString
    return [self my_URLWithString:[URLString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
}

+ (void)load {
    // 为了确保只执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        Method originalMethod = class_getClassMethod(self, @selector(URLWithString:));
        Method swizzledMethod = class_getClassMethod(self, @selector(my_URLWithString:));
        
        /*
         class_addMethod:如果类中存在方法,则添加失败。如果不存在则添加成功
         
         判断是否添加成功的目的:
            如果本类中没有实现originalMethod,但是父类中实现了。
            直接使用method_exchangeImplementations进行交换,
            交换的两个方法就是父类中的originalMethod和swizzledMethod。
            那么父类的其他子类调用originalMethod也会执行swizzledMethod。
            进行判断就是为了避免这种情况以及带来的其他麻烦
         
         在本分类中,因为确定URLWithString一定实现了,可以直接使用method_exchangeImplementations进行交换
         */
        BOOL didAddMethod = class_addMethod(object_getClass(self), @selector(URLWithString:), method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            // class_replaceMethod:替换方法的实现
            class_replaceMethod(self, @selector(my_URLWithString:), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }else {
            // 将两个方法交换
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        
    });
}

@end

这样直接使用[NSURL URLWithString:urlStr]就可以了,不必再去理会urlStr中是否存在中文。

参考:
iOS 模块分解—「Runtime面试、工作」看我就 🐒 了 _.
Objective-C Method Swizzling
Objective-C Runtime
iOS runtime和runloop

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

推荐阅读更多精彩内容

  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,550评论 33 466
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 729评论 0 2
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,132评论 0 9
  • 一、Runtime简介 Runtime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消...
    林安530阅读 1,062评论 0 2
  • Runtime是什么 Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我...
    SuAdrenine阅读 874评论 0 3