Objective-C的Runtime机制

苹果官方文档查找地址:https://developer.apple.com/library/mac/navigation/
Runtime官方文档https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html

简介

Objective-C是基于C加入了面向对象特性和消息转发机制的动态语言,除编译器之外,还需用Runtime系统来动态创建类和对象,进行消息发送和转发。Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的。比如:

OC中方法的调用:
[receiver message];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector)
// 如果其还有参数比如:
[receiver message:(id)arg...];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector, arg1, arg2, ...)
id objc_msgSend ( id self, SEL op, ... );

以上你可能看不出它的价值,但是我们需要了解的是 Objective-C 是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。

Runtime 的作用

Objc 在三种层面上与 Runtime 系统进行交互:

1.通过 Objective-C 源代码
2.通过 Foundation 框架的 NSObject 类定义的方法
3.通过对 Runtime 库函数的直接调用

Objective-C 源代码

多数情况我们只需要编写 OC 代码即可,编译时Runtime 系统自动在幕后搞定一切,还记得简介中如果我们调用方法,编译器会将 OC 代码转换成运行时代码,在运行时确定数据结构和函数。

通过 Foundation 框架的 NSObject 类定义的方法

Cocoa 程序中绝大部分类都是 NSObject 类的子类,所以都继承了 NSObject 的行为。(NSProxy 类时个例外,它是个抽象超类)

一些情况下,NSObject 类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如 -description 方法,该方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject 类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject 的子类可以重新实现。

还有一些 NSObject 的方法可以从 Runtime 系统中获取信息,允许对象进行自我检查。例如:

  • -class方法返回对象的类;
  • -isKindOfClass: 和 -isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
  • -respondsToSelector: 检查对象能否响应指定的消息;
  • -conformsToProtocol:检查对象是否实现了指定协议类的方法;
  • -methodForSelector: 返回指定方法实现的地址。
通过对 Runtime 库函数的直接调用

Runtime 系统是具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下,这意味着我们使用时只需要引入objc/Runtime.h头文件即可。

许多函数可以让你使用纯 C 代码来实现 Objc 中同样的功能。除非是写一些 Objc 与其他语言的桥接或是底层的 debug 工作,你在写 Objc 代码时一般不会用到这些 C 语言函数。对于公共接口都有哪些请参考苹果官方的 API 文档。

一些 Runtime 的术语的数据结构

要想全面了解 Runtime 机制,我们必须先了解 Runtime 的一些术语,他们都对应着数据结构。

SEL

它是selector在 Objc 中的表示(Swift 中是 Selector 类)。selector 是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意 Objc 在相同的类中不会有命名相同的两个方法。selector 对方法名进行包装,以便找到对应的方法实现。它的数据结构是:

/// An opaque type that represents a method selector.(表示方法选择器的不透明类型)
typedef struct objc_selector *SEL;

两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在 Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。

不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。

工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去 找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了。
我们可以通过以下方法获取SEL

1.SEL sel_registerName(const char *str)//向runtime system注册一个方法名。如果方法名已经注册,则放回已经注册的SEL
2.SEL sel_getUid(const char *str)//同上
3.@selector(<#selector#>)//oc编译器提供的
4.SEL NSSelectorFromString(NSString *aSelectorName)//OC字符串转化
5.SEL method_getName ( Method m );//根据Method结构体获取
等等
id

objc_msgSend第一个参数的数据类型id,id 是一个参数类型,它是指向某个类的实例的指针,id是通用类型指针,能够表示任何对象

typedef struct objc_object *id;
struct objc_object { Class isa; };

id其实就是一个指向objc_object结构体指针,它包含一个Class isa成员,根据isa指针就可以顺藤摸瓜找到对象所属的类.

注意:根据Apple的官方文档Key-Value Observing Implementation Details提及,key-value observing是使用isa-swizzling的技术实现的,isa指针在运行时被修改,指向一个中间类而不是真正的类。所以,你不应该使用isa指针来确定类的关系,而是使用class方法来确定实例对象的类。

Class
typedef struct objc_class *Class;

Class 其实是指向 objc_class 结构体的指针。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;
    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;

从 objc_class 可以看到,一个运行时类中关联了它的父类指针、类名、成员变量、方法、缓存以及附属的协议。

其中 objc_ivar_list 和 objc_method_list 分别是成员变量列表和方法列表:

// 成员变量列表
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
// 方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

由此可见,我们可以动态修改 *methodList 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因。这里可以参考下美团技术团队的文章:深入理解 Objective-C: Category

成员变量和属性的区别:@property声明的属性默认会生成一个“_”类型的成员变量,同时也会生成setter/getter方法。

objc_ivar_list 结构体用来存储成员变量的链表,而 objc_ivar 则是存储了单个成员变量的信息;同理,objc_method_list 结构体存储着方法数组的链表,而单个方法的信息则由 objc_method 结构体存储。

值得注意的时,objc_class 中也有一个 isa 指针,这说明 Objc 类本身也是一个对象。为了处理类和对象的关系,Runtime 库创建了一种叫做 Meta Class(元类) 的东西,类对象所属的类就叫做元类。Meta Class 表述了类对象本身所具备的元数据。

我们所熟悉的类方法,就源自于 Meta Class。我们可以理解为类方法就是类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。

当你发出一个类似 [NSObject alloc]类方法的消息时,实际上,这个消息被发送给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类(Root Meta Class)的实例。所有元类的 isa 指针最终都指向根元类。

所以当 [NSObject alloc] 这条消息发送给类对象的时候,运行时代码 objc_msgSend() 会去它元类中查找能够响应消息的方法实现,如果找到了,就会对这个类对象执行方法调用。

87C124E5-491E-4D0C-A386-FE5161537C98.png

上图实现是 super_class 指针,虚线时 isa 指针。有几个关键点需要解释以下:

  1. Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
  1. 每个Class都有一个isa指针指向唯一的Meta class。
  2. Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
  3. 每个Meta class根元类的isa指针都指向Root class (meta)。

最后 objc_class 中还有一个 objc_cache ,缓存,cache用来缓存经常访问的方法,它指向objc_cache结构体,它的作用很重要,后面会提到。

IMP

IMP实际上是一个函数指针,指向方法实现的首地址。当你向某个对象发送一条信息,可以由这个函数指针来指定方法的实现,它最终就会执行那段代码,这样可以绕开消息传递阶段而去执行另一个方法实现。其定义如下:

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

第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。

IMP原理:SEL是方法名的唯一标识,相同的方法名如果要对应不同的实现还需关联不同的实现地址,IMP就是为此而生的。当我们知道一个IMP时候就会知道他对应的SEL名。

IMP的作用:
我们就可以像调用普通的C语言函数一样来使用这个函数指针 了。
通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。

获取IMP的函数如下

IMP imp_implementationWithBlock(id block)//根据代码块获取IMP,其实就是代码块与IMP关联
IMP method_getImplementation(Method m) //根据Method获取IMP
[[objc Class] instanceMethodForSelector:SEL]//根据OC方式获取IMP
//待续

当我们获取一个方法的IMP时候可以直接调用IMP

IMP imp = method_getImplementation(Method m);
id objc = imp(id,SEL,argument);//objc用来保存方法的返回值,id表示调用这个方法的对象,SEL是Method的选择器,argument是方法的参数。

注意:
1.对于一个对象方法其实他的第一个参数是对象本身,第二个参数开始被存放在IMP的…里面。所以对于不属于任何对象的IMP我们要直接调用的参数是从IMP的第一参数id算起,忽略SEL传nil,然后接着后面的第二第三个参数。对于对象方法(包括类方法,类方法对象其实就是元类)则我们可以对id和sel直接传nil,忽略他然后直接调用该对象方法达到像使用C函数一样使用对象方法。
2.在OC里面对象方法是至少两个参数的C函数,两个参数是背隐藏起来的,即对象的自身self和方法名_cmd,我们在开发中常用到self,但是很少用到_cmd。
3.用imp掉用方法时候参数一点要传全,没有值的可以传nil,否则会出现错误或者一些想不到的结果。

Method

Method定义如下:它主要是用于描述类里面的方法,代表类中某个方法的类型

typedef struct objc_method *Method;

objc_method结构体定义如下:

struct objc_method {
    SEL method_name                           OBJC2_UNAVAILABLE;//方法名
    char *method_types                        OBJC2_UNAVAILABLE;//参数返回值字符串描述
    IMP method_imp                            OBJC2_UNAVAILABLE;//方法的实现
}
objc_method 存储了方法名,方法类型和方法实现:

方法名 method_name 类型为 SEL
方法类型 method_types 是个 char 指针,存储方法的参数类型和返回值类型
method_imp 指向了方法的实现,本质是一个函数指针

我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了Method,SEL可以通过Method找到对应的IMP,从而调用方法的实现代码。

Method操作函数如下

方法操作主要有以下函数:

// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );

// 获取实例方法
Method class_getInstanceMethod ( Class cls, SEL name );

// 获取类方法
Method class_getClassMethod ( Class cls, SEL name );

// 获取所有方法的数组
Method * class_copyMethodList ( Class cls, unsigned int *outCount );

// 替代方法的实现
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );

// 返回方法的具体实现
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );

// 类实例是否响应指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );

objc_method_description

objc_method_description结构体用来描述Method的一些信息,这样我么就可以直接读出来

struct objc_method_description {
    SEL name;               /**< The name of the method 方法名*/ 
    char *types;            /**< The types of the method arguments 参数类型字符串*/
};

获取函数

// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
Ivar

Ivar表示类中的实例变量,在runtime.h文件中找到它的定义:

/// An opaque type that represents an instance variable.  
typedef struct objc_ivar *Ivar; 
  
struct objc_ivar {  
    char *ivar_name                                          OBJC2_UNAVAILABLE;  
    char *ivar_type                                          OBJC2_UNAVAILABLE;  
    int ivar_offset                                          OBJC2_UNAVAILABLE;  
#ifdef __LP64__  
    int space                                                OBJC2_UNAVAILABLE;  
#endif  
}  

Ivar其实就是一个指向objc_ivar结构体指针,它包含了变量名(ivar_name)、变量类型(ivar_type)等信息。

Cache

顾名思义,Cache主要用来缓存,那它缓存什么呢?我们先在runtime.h文件看看它的定义:

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

Cache其实就是一个存储Method的链表,主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用

可以通过class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

注意:
返回的是属性列表,列表中每个元素都是一个 objc_property_t 指针

#import <Foundation/Foundation.h>

@interface Person : NSObject

/** 姓名 */
@property (strong, nonatomic) NSString *name;

/** age */
@property (assign, nonatomic) int age;

/** weight */
@property (assign, nonatomic) double weight;

@end

以上是一个 Person 类,有3个属性。让我们用上述方法获取类的运行时属性。

    unsigned int outCount = 0;

    objc_property_t *properties = class_copyPropertyList([Person class], &outCount);

    NSLog(@"%d", outCount);

    for (NSInteger i = 0; i < outCount; i++) {
        NSString *name = @(property_getName(properties[i]));
        NSString *attributes = @(property_getAttributes(properties[i]));
        NSLog(@"%@--------%@", name, attributes);
    }
打印结果如下:

2017-02-13 16:54:08.684 Runtime体验[7435:176645] 3
2017-02-13 16:54:08.684 Runtime体验[7435:176645] name--------T@"NSString",&,N,V_name
2017-02-13 16:54:08.684 Runtime体验[7435:176645] age--------Ti,N,V_age
2017-02-13 16:54:08.684 Runtime体验[7435:176645] weight--------Td,N,V_weight

property_getName 用来查找属性的名称,返回 c 字符串。property_getAttributes 函数挖掘属性的真实名称和 @encode 类型,返回 c 字符串。

objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

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

推荐阅读更多精彩内容