OC-Runtime的理解和简单使用

Runtime是OC里面非常重要的一个概念,它是OC的底层实现,也正是因为Runtime,OC成为一个动态语言,并且拥有了面向对象的能力。这篇文章,将详细说明Runtime的各种知识,并且能够实际运用。

  • 什么是Runtime

    Runtime即运行时,也就是程序在运行的时候做的事情。在iOS中,有一套底层的C语言API,它是iOS内部核心之一,我们编写的OC代码,底层都是基于它实现的,OC语言在编译后,都是Runtime形式的C语言代码。

学习Runtime可以使我们更加清楚地了解OC语言的底层实现,从而可以运用它去实现很多OC语言实现不了的功能(比如给Category添加属性)。

  • 类和对象

在没有接触Runtime之前,我们对OC的类和对象只有概念上的理解,并不知道它本质上是什么。现在我们来看看它们的底层定义,我们先从我们经常使用的id入手,在<objc/objc.h>中,我们找到这一段定义:

/// A pointer to an instance of a class.
typedef struct objc_object *id;

这里说明id是一个指向objc_object结构体的指针,而注释又说,这个指针指向一个类的实例对象,所以我们知道了,objc_object结构体就代表了一个类的实例对象:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

objc_object这个结构体里面,只有一个isa,这个isa的类型是Class,并且是不能为空的。顾名思义,这个肯定就是这个对象的类型了。
然后我们再去找找Class又是什么东西:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

这里我们可以看到,Class是一个指向objc_class结构体的指针。所以我们知道了,isa是一个指向objc_class的指针,即通过它可以找到一个对象的类。所以,id类型其实就是一个指向任意类型实例的指针。接着,我们再去看看objc_class是什么东西:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父类

    const char *name                        OBJC2_UNAVAILABLE;  // 类名
    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
    long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识

    long instance_size                      OBJC2_UNAVAILABLE;  // 类的实例变量大小
    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 类的成员变量链表

    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存

    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
#endif

} OBJC2_UNAVAILABLE;

我们先从第一行可以得知,OC的类里面,也有一个isa指针,而这个指针指向了这个类的类型,由此我们可以推断出来,其实OC的类本质上也是一个对象,因为它也有自己的类。
在OC里面,每一个类的isa指针都指向它的元类,最终指向NSObjectNSObject的元类是它自己。而NSObject的父类则是nil。这张图很好的说明了isasuper_class的区别:


接着,我们可以看到,一个类的结构体里面,还有它的父类,类名,版本号,类信息,变量大小,变量列表,方法列表,方法缓存,协议列表这些东西。
这里也解释了为什么前面的objc_object里面只有一个isa指针,是因为只要有了这个指针,就能够找到这个类里面的所有方法和属性。
看了这些,我们就对OC的类和对象有了更深刻的理解。接下来,我们再去探究一下OC是怎么调用方法的。

  • OC的消息发送

OC的方法调用底层是给某个对象发送某个方法。并且,在编译的时候并没有确定具体调用哪个方法,只有在运行时才能确定。我们来验证一下,首先随便创建一个空的命令行项目:



然后创建一个Person类,然后写一个空的方法:

.h
@interface Person : NSObject

- (void)run;

@end

.m
- (void)run {
    
}

然后打开终端,cd到刚刚创建的main.m文件所在的文件夹下,执行clang -rewrite-objc main.m
这时候,就可以在文件夹里面看到一个main.cpp文件,打开,拉到最下面,就可以看到这一段代码:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
    }
    return 0;
}

可以看到我们刚刚写的方法编译过之后全部变成了objc_msgSend的方式,甚至allocinit方法也变成了这样。这就说明OC方法的底层调用都是objc_msgSend实现的,而objc_msgSend就是消息发送。同时,也验证了OC底层就是用Runtime实现的C语言代码。

接下来我们来研究一下这个objc_msgSend。要使用它,得首先导入#import <objc/message.h>,然后就可以使用了。但是,我们打出来这个发现没有任何参数提示:


这是因为苹果公司不建议我们这么用了。我们可以在Build Setting里面,找到

改成NO,再回去敲入objc_msgSend,就可以看到提示了:

具体的参数含义是:
id _Nullable selfid类型我们前面知道,它可以指向任意OC对象,这地方就代表着给谁发消息,也就是调用谁的方法。
...:三个点代表参数列表/可扩展参数。
SEL _Nonnull op:SEL又是什么呢?到这里,我们就得提一下OC里面的方法了。老规矩,我们先去找定义,在<objc/runtime.h>中:

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

这里我们可以看到,OC的方法里面,有三个东西,一个是SEL,一个是char * ,一个是IMP,这个char *我们知道是C语言的字符串,这个地方是一组描述方法的参数类型的字符数组,后面我们会详细了解这个东西,这里先不管它。SEL这个地方通过命名我们可以看出来是方法名,IMP我们就完全看不出了,它们具体是怎么定义的呢?还是在<objc/objc.h>中:

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

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

这里,我们可以看到,IMP其实就是一个指向方法实现的指针,而SEL则是一个objc_selector结构体,源码中我们找不到SEL的定义,经过查阅资料得知,它完全可以理解为一个char *,也就是说,其实它就是方法名的字符串,也就是一个方法的标签。
知道这些以后,我们就可以用消息发送来改写以前的OC代码,比如:
前面的Person类的run方法的调用:

objc_msgSend(p, @selector(run));

为了方便测试,我们给run方法写一个简单的实现:

- (void)run {
    NSLog(@"跑了");
}

然后运行一下:



完美!

甚至Person类对象的声明都可以用发送消息的方式来完成(解耦合):

Person *p = objc_msgSend([Person class], @selector(alloc));
p = objc_msgSend(p,@selector(init));

再运行一下,一样可以得到之前的结果,这里就不贴图了,跟上面那个图一样。
Runtime为我们提供了直接通过类名获取类的函数:

objc_getClass(char * _Nonnull name);

和得到一个SEL的函数:

sel_registerName(const char * _Nonnull str);

这样我们可以继续改进之前的代码:

id p = objc_msgSend(objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")),sel_registerName("init"));
objc_msgSend(p,sel_registerName("run"));;

一样可以运行得到结果。
到了这一步,是不是就跟我们之前看到的编译后的OC代码一样了?我们甚至不需要导入Person.h头文件,就可以直接获取创建它的实例,并且执行方法,完成了解耦。

  • OC的消息转发(message forwarding)

看了上面的一些代码,不知道你有没有考虑过一个问题。发送消息的时候,我们只需要填一些字符串参数之类的就可以了,完全不知道有没有这个方法,如果没有这个方法会发生什么事情呢?
接下来,我们做个试验:



直接crash掉了。调用方法时,如果在方法在对象的类继承体系中没有找到,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出 unrecognized selector sent to …类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。这就涉及到以下4个方法:

  • 动态方法解析(dynamic method resolution)

首先会调用+ resolveInstanceMethod:(对应实例方法)或+ resolveClassMethod:(对应类方法)方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
我们这里测试一下,增加一个wahaha方法的实现,看看是否可以顺利运行,首先,我们在Person.m中导入#import <objc/message.h>,然后,利用

class_addMethod(Class  _Nullable __unsafe_unretained cls,SEL  _Nonnull name,IMP  _Nonnull imp, const char * _Nullable types)

来增加一个方法及实现,其中,第一个参数填self,第二个参数填wahaha的SEL,可以用@selector(wahaha),也可以用之前用过的sel_registerName("wahaha"),第三个参数需要一个imp,我们知道IMP是指向方法实现的指针,这里我们可以用imp_implementationWithBlock(id _Nonnull block)来实现,最后一个参数我们之前也见过,就是方法定义里面的的method_types,这个东西该怎么写呢?我们先去查一下官方文档:

types
An array of characters that describe the types of the arguments to >the method. For possible values, see Objective-C Runtime >Programming Guide > Type Encodings. >Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).

这里面说明这个是一组描述方法的参数类型的字符数组,并且,每个方法都有两个被隐含的参数,一个是self(代表当前对象),一个是_cmd(代表当前对象的SEL),所以第二个和第三个字符必须是@:,而第一个字符是返回值,所以一个没有参数的方法,它的types就是"v@:"。至于什么类型对应什么字符,可以去上面的链接中找。所以我们这里可以直接用"v@:"

//如果增加了方法并返回YES,就会重新发送消息并处理,返回NO,则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == sel_registerName("wahaha")) {
        class_addMethod(self, sel_registerName("wahaha"), imp_implementationWithBlock(^(){
            NSLog(@"wahaha");
        }), "v@:");
    }
    return YES;
}

运行结果:


怎么样,是不是很神奇?如果上面返回NO,则会进入完整的消息转发机制(full forwarding mechanism),这里又分为两个步骤:

  • 快速消息转发 (Fast Forwarding)

这个时候,如果实现了- forwardingTargetForSelector:方法,系统就会进入该方法继续处理消息,这个方法的作用是把之前没办法处理的消息转发给别的对象去处理:

//返回一个对象继续处理消息
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == sel_registerName("wahaha")) {
        return [Dog new];
    }
    return nil;
}

这里我们新建了一个Dog类,实现了wahaha方法,所以我们直接返回一个Dog的实例,最后运行结果如上,这里就不贴图了。

  • 普通消息转发(Normal Forwarding)

如果上一步也没有对消息进行处理,则会进入最后一步,这里涉及到两个方法。它首先调用methodSignatureForSelector:方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:方法。我们同样在这里对之前的消息进行处理一次:

//返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == sel_registerName("wahaha")) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//转发消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    Dog *dog = [Dog new];
    if ([dog respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:dog];
    }
}

以上就是OC消息传递过程中发生的事情,利用这些我们可以在很多地方对一个消息做处理。但是我们该怎么选择呢?

  1. 动态方法解析:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
  2. 快速消息转发:其他对象,使用范围更广,不只是限于原来的对象。
  3. 普通消息转发:它一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。

同时需要注意的是,消息转发过程中,步骤越往后,处理消息的代价就越大,最好能在第一步就处理完,这样的话,运行期系统可以将此方法缓存。如果这个类的实例还会再接收到同名选择子,那么根本无须再次启动消息转发流程。
通过消息发送,我们其实已经对Runtime做了一个简单的运用了。接下来,我们再多一些探讨。

  • Runtime可以做什么?

  1. 在程序运行的时候动态添加一个类
  2. 在程序运行的时候动态的修改一个类的属性和方法
  3. 在程序运行的时候遍历一个类的所有属性

Runtime有很多方法,可以在文档中一一查看,不同功能的方法通过前缀区分,比如说class_就是对类的操作,objc_就是对对象的操作,等等,都比较好理解。

了解这些以后,我们再回头看之前提过的一个问题,就是给Category增加属性
我们先看一下,如果直接给Category增加属性会发生什么,我们给Person类创建一个Play分类,然后添加一个属性:

@interface Person (Play)

@property (nonatomic, strong) NSString *gameName;

@end

但是我们使用的时候会发现,直接就Crash了:


我们现在应该就能明白,这是因为找不到Getter,同时也没有Setter,我们可以给它添加这两个方法,但是我们在给它添加的时候,发现在Category里面根本就没有_gameName这个变量,所以没办法像别的类那样直接添加,这时候,我们就可以利用Runtime的objc_setAssociatedObjectobjc_getAssociatedObject来实现:

- (NSString *)gameName{
    return objc_getAssociatedObject(self, _cmd);
}
- (void)setGameName:(NSString *)gameName {
    objc_setAssociatedObject(self, @selector(gameName), gameName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

再次运行,就可以正常使用这个属性了:

通过以上简单的例子,我们可以看得出来,Runtime是OC代码的底层实现,所以很多OC代码不支持的事情,我们都可以通过Runtime自己去实现,具体在什么场景下使用要根据实际需求做具体分析。接下来,我会用Runtime和之前Block种提到的相关技术自己实现KVO,并且进行一些改造。另外,本篇文章的代码可以在我的github上查看,点击前往

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,097评论 0 9
  • Runtime是什么 Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我...
    SuAdrenine阅读 839评论 0 3
  • 宝宝,冥王星又要中断一下下了,由于生病体虚,晚上基本不能写,今天又忙着来还书,都已经过期了,要交罚款2刀。 发现图...
    Hayeknz阅读 153评论 0 0
  • 本文通过解析Class文件中字节码的结构,来加深对Java类文件结构的理解。建议先阅读Java类文件结构解析这篇文...
    tianbin阅读 384评论 0 0