iOS开发之Runtime

在swift这门优雅的语言还没诞生之前,iPhone开发主要使用的是Object-C这门面向对象语言,OC是由C实现的超集(大部分的OC库都有对应的C版本的实现例如Foundation和CoreFoundation),并不需要像JAVA那样运行在虚拟机中,而且可以很好的结合C和C++代码提高程序的性能,除了面向对象的特性外,OC这门语言还具备了smalltalk的消息机制,当我们调用了一个对象的方法或者说函数时,其实是向那个对象发送了一条消息。
OC是一门动态语言,也就是说在OC运行时,有一个运行时系统,运行时系统的作用就是执行编译后的代码,动态的加载类,向对象发送消息,运行时系统更像是OC的操作系统。
那么什么是动态呢?我们来看看下面这段代码:

Person *p = [[Person alloc] initWithName:@"Tom" andAge:15];
[p performSelector:@selector(sayHello)]; //虽然Person类中并没有这个sayHello方法,依然可以编译通过

这段代码在编译阶段并不能够判断出Person对象是否存在sayHello这个方法(尽管会给出警告,但并不报错),可以通过编译阶段,但是会在运行时崩溃。也就是说OC语言的动态特性使得类型信息在运行时被检查,而不是编译时。同时Class也是动态创建的,也就是说你可以在程序运行的时候为程序新增类、对象、以及方法和方法体等。本文将介绍runtime原理和实际应用:

1. 消息机制

2. 消息转发

3. 属性定义

4. 实际使用

在了解Runtime机制之前,先来简单了解一下NSObject这个公共父类(并不是所有的类都继承自NSObject,例如NSProxy):
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
+ (void)load;
+ (void)initialize;
- (instancetype)init
#if NS_ENFORCE_NSOBJECT_DESIGNATED_INITIALIZER
NS_DESIGNATED_INITIALIZER
#endif
.
.
.

可以看到,NSObject有一个成员变量叫做isa,它是Class类型的,这个Class其实是一个结构体:typedef struct objc_class *Class,来研究一下这个结构体:

struct objc_class {
    Class isa OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class ;
        const char *name ;
        long version ;
        long info ;
        long instance_size ;
        struct objc_ivar_list *ivars ;
        struct objc_method_list **methodLists ;
        struct objc_cache *cache ;
        struct objc_protocol_list *protocols ;
    #endif
} OBJC2_UNAVAILABLE;

结构体的成员中包含了另一个isa的引用,其它的结构成员在OC2.0之后不可用,但是,依然可以从中获取重要的一些信息,例如父类、类名、对象大小、变量列表、方法列表、该类遵循的协议等都是以列表的形式保存在objc_class中,其中还有一个缓存cache,用于缓存最近使用到的消息,该文件中还包括对其它结构体的定义,例如方法、类目、属性等。(可以通过#import<objc/runtime.h>查看)基础知识先说到这里,来看看消息机制。

1.消息机制

什么是消息机制,举例来说:

Math m = [[Math alloc]init];
[m sum:5 y:6];

通常会说调用了m对象的sum方法,但编译器会将函数调用转变为向对象发送一条消息,:
objc_msgSend(m , sum ,5 , 6);
现在应该说是像m对象发送了一条sum消息更合适。

首先应该了解SEL和IMP,我们暂且可以这么区分,一个方法有方法名和方法体,SEL指的是方法名(可以这么理解但是实际叫做<b>选择器</b>,下文会提到),而IMP指的是方法体也就是对应的实现,在C语言中例如调用一个方法的话,编译器会将方法调用转换为汇编指令call 并带一个地址操作数,程序计数器会将下一条要执行的指令地址设为这个操作数,并将返回地址压入栈中。在OC中SEL的定义为:typedef struct objc_selector *SEL;而IMP的定义为:typedef void (*IMP)(void /* id, SEL, ... */ );

在运行时,消息会绑定到对应的实现上:

  • 首先会根据对象m的选择器sum查找对应的方法实现(IMP)

  • 传递参数(包括消息的接受对象、选择器),执行方法

  • 将函数的返回结果返回

    当一个新的对象被创建时,系统要为该对象分配对应的内存,实例变量被初始化,还记得上面说的isa么,isa这个指针变量将被指向该对象的<b>类结构</b>,之后通过super_class可以获取该对象的父类,进而整个继承链的类结构信息就都可以获取了。编译器负责将类、对象构建为具有运行时信息的结构(包括isa、super_class、选择器转发表等)。另一个重要的信息就是转发表,可以看做是一个以SEL为键以IMP地址为值的映射。
    当向一个对象发送消息时,objc_msgSend会去该对象的类结构体(isa指针指向的结构)中查找转发表,如果能够定位指定的选择器的话,就会执行对应地址处的方法体,如果找不到,会沿着继承链一层层去查找每一个父类的转发表直到NSObject类。如下图所示:

23-22-32.jpg
这样一层层查找会有损程序效率,于是就有了上面提到的缓存,当第一次调用了某个方法,系统便会将方法和对应的实现地址缓存起来(系统就是如此霸道,一旦第一次使用了某个方法,系统会认为你还想继续使用),在查找转发表之前,会搜一搜这个缓存。
用上面的Math来说明这个过程,当我们创建m对象时,系统会为m 对象分配内存,将isa 指针指向Math 类结构,并配置转发表,当我们向m 发送sum 消息时,objc_msgSend 会去isa 所指的结构体中查找转发表,去找啥?去找selector 为sum 的地址,如果找到,就执行对应地址的方法体,然后将selector 缓存起来,如果找不到,就沿着superClass 链向上查找转发表,如果到了NSObject 这一层还没有找到,对不起,程序就抛异常了。

objc_msgSend至少需要两个参数:接收消息的对象和选择器,这两个参数是在编译的时候被插入的,在OC中我们定一个方法,并不需要显示的指定这两个参数。
刚才说了那么多selector和IMP地址,那么如果我们不想通过消息机制来调用一个函数应该怎么办,可以通过methodForSelector:SEL来将方法的实现取出来:

Student *p = [[Student alloc] initWithName:@"Tom" andAge:15];
typedef NSInteger(*sum)(id ,SEL , NSInteger , NSInteger);
sum s = (sum)[p methodForSelector:@selector(sum:y:)];
NSLog(@"%d",s(p , @selector(sum:y:) , 10 , 5));

输出结果为15。注意methodForSelector返回的是IMP结构体,需要转换为指定函数指针类型,并保证前两个参数依然为接受对象和方法选择器。

2. 消息转发

有没有过这样的经历,当我们试图调用一个不存在的方法是,会报以下的错误:unrecognized selector sent to instance 0x1004001a0,很多iOS初学者不知道这句话是什么意思,是说地址为0x1004001a0的对象没有定义相关的方法选择器,因此不能够被识别,通过查看该地址处的对象就可以找到出错的原因,也可以根据debug的crash信息定位出错的对象和信息。那么如果一个对象无法响应某个消息(就是上面说的没有定义某个函数),运行时向该对象发送了这个未定义的消息,程序就一定被判死刑了么,其实不一定,当一个对象无法响应某个message的时候,系统会给你三次机会来动态的为一个对象增加一个处理消息的实现或者实现消息的转发,让我们来看看第一种方式:

动态决议

+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel

这两个方法都是动态的为方法选择器添加方法实现又叫做<b>动态方法决议</b>,当调用了对象的一个不存在的方法选择器或者该方法选择器没有对应的方法体,消息机制会调用这两个方法来决议(注意:如果消息机制沿着继承连找不到对应的selector和IMP之间的映射时才会调用,也就是说只有调用了类或者对象不存在的方法体时才会尝试决议),例如:

//Person 类
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic , strong)NSString *name;
@property(nonatomic , assign)NSInteger age;

- (id)initWithName:(NSString *)name andAge:(NSInteger)age;
- (void)say;
- (NSInteger)sum:(NSInteger)x y:(NSInteger)y;
+ (void)sayHello;
@end

对应的implement为:

// implement
#import "Person.h"
@implementation Person
- (id)initWithName:(NSString *)name andAge:(NSInteger)age {
    if(self = [super init]){
        self.name = name;
        self.age = age;
    }
    return self;
}
+ (void)sayHello {} //1
+ (BOOL)resolveClassMethod:(SEL)sel { //2
    NSLog(@"%@",NSStringFromSelector(sel));
    if(sel == @selector(sayHello)) {
            return YES;
    }
    return [Person resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%@",NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}
@end

以类方法sayHello 为例,此时已经实现了sayHello,所以并不会调用resolve方法,当我们把1处的代码删除后,程序会崩溃,如何动态为类方法sayHello添加方法体呢?
我们将2处的方法修改为:

+ (BOOL)resolveClassMethod:(SEL)sel {
    if(sel == @selector(sayHello)) {
        class_addMethod([NSObject class], @selector(sayHello),         (IMP)sayHelloDynamic, "v@:");//①
        return YES;
    }
    return [super resolveClassMethod:sel];
}

然后添加如下代码:

void sayHelloDynamic(id target , SEL sel) {
    printf("Hello world\n");
}

执行结果为:Hello world。(此处有疑问:上面代码的①处,必须指定为NSObject的类对象,如果是Person的话,通过class_getClassMethod得到的结果为nil,也就是说添加类方法不成功,这里还需要继续调查,如果有知道的读者可以留言。)
resolveClassMethod动态添加类方法,而resolveInstanceMethod动态添加实例方法,网上大多数教程都在解释后面这个方法,想必也是因为resolveClassMethod添加类方法失败。

消息转发

当通过继承连定位不到对象相关消息的实现,同时resolveInstanceMethod对应的selector返回NO的话,系统会尝试消息转发(按照文档的说法,决议优先消息转发,决议与消息转发正交,也就是如过对应的selector在决议方法中返回true,消息转发不会被调用)。可以将消息转发看做是处理不存在消息的第二层保护。如果向一个对象发送了一个它不能处理的消息时,运行时系统会向forwardInvocation:发送一个消息,并传递NSInvocation对象,该对象可以看做是一个方法调用的包装(消息的响应对象、selector、参数、返回值),通过重写该方法就可以获得一个消息转发的机会。
还是上面的Person类,如果我们现在调用Person对象的say方法,程序一定崩溃,让我们在implement中加入以下代码:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if(![self respondsToSelector:anInvocation.selector]){
        return;
    }
}

但是单单是重写了forwardInvocation方法还是不够的,还需要重写methodSignatureForSelector:方法来为forwardInvocation:的anInvocation参数提供必要的信息,代码如下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"----%@",NSStringFromSelector(aSelector));
    if([self respondsToSelector:aSelector]) {
        return [super methodSignatureForSelector:aSelector];
    } else {
        [self noMessage:aSelector];
    return [NSMethodSignature signatureWithObjCTypes:"v@:"]; }
}

- (void)noMessage:(SEL) sel{
    NSLog(@"No this function %@",NSStringFromSelector(sel));
}

我们使用signatureWithObjCTypes创建NSMethodSignature对象,这里需要传一个参数,就是函数的编码类型,由返回值类型、参数类型决定,可以参看官方的图解:


23-35-44.jpg

例如我们定一个函数void sum(int x , int y)那么这个函数的编码就为"vii",如果是void msgHand(id target , SEL selector)这个函数的编码为"v@:"这种格式是消息实现体IMP常使用的格式。
以上代码我们并没有转发消息,而是将不能处理的消息打印出来并swallow掉,如果想实现转发的话,可以转换为如下代码,只需要修改forwardInvocation的代码就可以了:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(say)){
        Student *stu = [[Student alloc] init];
        [anInvocation invokeWithTarget:stu];
    }
}

这样就把消息转发给了stu对象了,可以看出来,OC的对象可以作为消息转发的中心,也可作为错误消息的垃圾站,如上述实现。

3. 属性定义

当编译器遇到属性声明时(@property),将会生成一个关于此属性的原型数据,我们可以通过系统的api获取一个类、协议中的属性以及其对应的原型。

typedef struct objc_property *objc_property_t;

属性也是结构体指针,但是该结构体不可见,只能通过相关函数获取内部的信息。
获取一个类和协议的全部属性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)//获取一个类的全部属性
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)//获取协议中的全部属性
const char *property_getName(objc_property_t property) //返回属性名
const char *property_getAttributes(objc_property_t property) //返回属性的编码类型信息

以下函数获取Person类的全部属性和属性的类型信息:

- (void)demo {
    unsigned int num;
    objc_property_t *properties = class_copyPropertyList([self class], &num); //1
    for (int i = 0 ; i < num; i++) { //2
        objc_property_t one = properties[i];
        NSString *attrName = [NSString stringWithCString:property_getName(one)       encoding:NSUTF8StringEncoding]; //3
        NSString *typeString = [NSString stringWithCString:property_getAttributes(one) encoding:NSUTF8StringEncoding]; //4
        NSLog(@"attr is %@ , type is %@",attrName , typeString);
    }
    free(properties);
}

1 处获取Person类的全部属性并保存在properties数组中,并将数组长度保存在num中。
2 循环遍历properties
3 4 获取属性名和属性的编码类型信息
输出结果:

attr is name , type is T@"NSString",&,N,Gname,V_name
attr is age , type is Tq,N,V_age

当然我们可以在程序运行的时候动态地添加属性:

BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)

属性的类型信息:
以T开始后接类型的@类型加一个',',由V_属性名结束,中间就是该属性的描述符,用,号隔开。附赠一张苹果官方的属性类型编码:


23-28-28.jpg

4. 实际使用

Runtime的使用比较多样也比较灵活,但是比较流行的用法就是method swizzling,也就是互换方法体,如下图所示在进行method swizzling前后selector和IMP之间的关系:

Group 2.png

Group 3.png

交换方法体之后的对应关系,在实际中有什么作用呢,例如项目开发了一大半,突然有一个采集数据的需求,需要每次用户进入页面都要统计每个页面进入的次数,由于项目已经接近尾声,再假如说没有做关于VC的同一调用接口,一个个页面修改起来就会很麻烦,如果我们能够在所有的VC执行viewDidAppear 时做一个其它的事情还不用每个页面都修改这是最好的办法,那么method swizzling就很适合你:

#import <UIKit/UIKit.h>

@interface UIViewController (Swizzle)

@end

#import "UIViewController+Swizzle.h"
#import <objc/runtime.h>
@implementation UIViewController (Swizzle)
+ (void)load{
    SEL selDidAppear = @selector(viewDidAppear:);
    Method impDidAppear = class_getInstanceMethod([self class], selDidAppear);
    SEL selLog = @selector(logVC:);
    Method impLog = class_getInstanceMethod([self class], selLog);
    method_exchangeImplementations(impDidAppear, impLog);
}

- (void)logVC:(BOOL) nouse {
    NSLog(@"进入了页面 %@", NSStringFromClass([self class]));
    [self logVC:nouse];
}
@end

这样VC的viewDidAppear和logVC的方法实现就交换了,当系统调用viewDidAppear实际调用的是logVC的方法体,而调用logVC实际走的是viewDidAppear。在实际中的应用还有很多例如数据统计、动态加载代码、为类目添加属性等。
项目代码:<a href='https://github.com/ChinaPicture/iOS-Runtime.git'>gitHub-iOSRuntime</a>

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

推荐阅读更多精彩内容