【iOS】Runtime与对象模型

一. Runtime是什么

Runtime 是一个共享动态库,其目录位于 /usr/include/objc,由一系列的C函数和结构体构成。和 Runtime 系统发生交互的方式有三种,一般都是用前两种∶

  • 使用OC源码 直接使用上层OC源码,底层会通过 Runtime 为其提供运行支持,上层不需要关
    /心 Runtime 运行。
  • NS0bject 在OC代码中绝大多数的类都是继承自 NSObject 的, NSProxy 类例外。 Runtime 在 NS0bject 中定义了一些基础操作, NSObject 的子类也具备这 些特性。
  • Runtime 动态库上层的OC源码都是通过 Runtime 实现的,我们一般不直接使用 Runtime ,直接和OC代码打交道就可以。

我们编写OC代码,底层都是基于它来实现的。比如

[receiver message];
// 底层运行时会被编译器转化:
objc_msgSend(receiver, selector)
// 如果其还有参数比如:
[receiver message:(id)arg...];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector, arg1, arg2, …)

二. runTime的对象模型

下面图中表示了对象间ISA关系,以及类的继承关系

在这里插入图片描述

从 Runtime 源码可以看出,每个对象都是一个 objc_object 的结构体,在结构体中有一个isa指针,该指针指向自己所属的类,由Runtime负责创建对象。
类被定义为 obic class 结构体, obic class 结构体继承自 obic object ,所以类也是对象。在应用程序中,类对象只会被创建一份。在 objc class 结构体中定义了对象的 method list、protocol、 ivar list 等,表示对象的行为。
既然类是对象,那类对象也是其他类的实例。所以 Runtime 中设计出了meta class,通过 meta class 来创建类对象,所以类对象的 isa指向对应的 meta class。而 meta class 也是一个对象,所有元类的 isa 都指向其根元类,根原类的 isa 指针指向自己。通过这种设计, isa 的整体结构形成了一个闭环。

// 精简版定义
typedef struct	objc_class	*Class;
struct	objc_class	:	objc_object	{
//	Class	ISA;
				Class	superclass;
}
struct	objc_object	{
				Class	_Nonnull	isa		OBJC_ISA_AVAILABILITY;
};

在对象的继承体系中,类和元类都有各自的继承体系,但
和元类都有各自的继承体系,但它们都有共同的根父类NSObject,
而NSObject的父类指向nil。
需要注意的是,上图中Root Class(Class)是NSObject类对象,
而RootClass(Meta)是NSObject的元类对象。

三.RunTime的作用
OC 在三种层面上与RunTime系统进行交互:

  • 通过OC源代码
    只要编写OC代码,RumTime系统自动在幕后搞定一切,调用方法,编译器将会OC代码转换成运行时代码,在运行时确定数据结构和函数
  • 通过Foundation框架的NSObject类定义方法
    cocoa程序中绝大多数类 都是NSObject类的 子类 ,所以都继承了NSObject的行为,(NSProxy类是例外,它是抽象超类)
    一些情况下 ,NSObject类仅仅定义完成某些事件的模板,并没有提供所需的代码。例如 - desciption 方法,该方法返回内容的字符串表示,该方法主要用来调试程序。NSObject类并不知道子类的内容,所以只是返回类的名字和对象的地址,NSObject的子类可以重新实现。
    还有一些NSObject的方法可以从RunTime系统中获取信息,允许对象进行自我检查,例如:
-class 方法返回对象的类;
-isKindOfClass:和-isMemberOfClass:方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
-respondsToSelector:检查对象能否响应指定的消息;
-conformsToProtocol:检查对象是否实现了指定协议类的方法;
-methodForSelector:返回指定方法实现的地址
  • 通过对RunTime库函数直接调用
    RunTime系统是具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下,这意味着我们使用时只需要引入objc/Runtime.h头文件即可。
    许多函数可以让你使用纯C代码实现Object中同样的功能。除非写一些Object与其他语言的桥接或者底层debug工作,你在写Object代码时一般不会用到这些语言函数

四.RunTime相关类对象

1.SEL
它是selector在 Objc 中的表示(Swift 中是 Selector 类)。selector 是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意 objc在相同的类中不会有命名相同的两个方法。selector 对方法名进行包装,以便找到对应的方法实现。它的数据结构是∶
typedef struct objc selector *SEL;
我们可以看出它是个映射到方法的 c 字符串,你可以通过 objc 编译器器命令@selector()或者 Runtime 系统的 sel registerName 函数来获取一个 SEL 类型的方法选择器。
注意∶ 不同类中相同名字的方法所对应的 selector 是相同的,由于变量的类型不同,所以不会导致它们调用方法实现混乱。

2.id
id 是一个参数类型,它是指向某个类的实例的指针。定义如下∶ typedef struct objc_ object *id; struct objc_object { Class isa; };
以上定义,看到 objc object 结构体包含一个 isa 指针,根据 isa 指针就可以找到对象所属的类。
注意∶ isa 指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的 -class 方法。PS∶KVo 的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实类型。

3.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 objcmethod_list {
struct objcmethod_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;
} 

4.Method
Method 代表类中某个方法类型

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                         
OBJC2_UNAVAILABLE;
    char *method_types                                      
OBJC2_UNAVAILABLE;
    IMP method_imp                                          
OBJC2_UNAVAILABLE;
}

objc_method存储了方法名,方法类型和方法实现:
方法名类型为 SEL
方法类型 method_types是个char指针,存储方法的参数类型和返回值类型
method_imp 指向了方法实现,本质是一个函数指针
Ivar
Ivar是表示成员变量的类型

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_offset 是基地址偏移字节

5.IMP
IMP在objc.h中的定义是∶
typedef id (*IMP)(id,SEL,…);
它就是一个函数指针,这是由编译器生成的。当你发起一个 objc 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。
如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面 Cache 中会提到。
你会发现 IMP 指向的方法与 objc msgSend 函数类型相同,参数都包含 id 和 SEL类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id和 SEL 参数就能确定唯一的方法实现地址。

6.Cache
Cache 定义如下:

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

Cache 为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在 isa指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。
Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache -样。

7.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 intoutCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)注意∶
返回的是属性列表,列表中每个元素都是一个 objc_property_t 指针#import<Foundation/Foundation.h>@interface Person : NSObject/*姓名 /
eproperty (strong,nonatomic)NSString name;/
age /
eproperty (assign,nonatomic) int age;/
weight */
eproperty (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);
    }

8.Runtime与消息
一些 Runtime 术语讲完了,接下来就要说到消息了。体会苹果官方文档中的 messages aren’t bound to method implementations until Runtime。消息直到运行时才会与方法实现进行绑定。
这里要清楚一点,objc_msgSend 方法看清来好像返回了数据,其实objc_msgSend 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。下面详细叙述消息发送的步骤∶

  • 首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
  • 检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。

如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。如果 cache 找不到就找类的方法列表中是否有对应的方法。
如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject类为止。
如果还找不到,就要开始进入动态方法解析了,后面会提到。
在消息的传递中,编译器会根据情况在 objc_msgSend , objc_msgSend_stret , objc_msgSendSuper,objc_msgSendSuper_stret 这四个方法中选择一个调用。如果消息是传递给父类,那么会调用名字带有 Super 的函数,如果消息返回值是数据结构而不是简单值时,会调用名字带有 stret 的函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值