iOS知识梳理8:万恶的Runtime

本文中所使用的参考链接:
ios开发-Runtime详解
ios Runtime几种基本用法简记
iOS运行时详解
ios runtime理解

1.Runtime

什么是Runtime
Runtime又叫运行时,是一套底层的C语言api,其为iOS内部的核心之一,我们平时编写的OC代码,底层都是基于它来实现的.
比如

[receiver message];
//带参数[receiver message:(id)arg,...];

底层编译时会被编译器转化为

objc_msgSend(receiver, selector);
//带参数obj_msgSend(receiver, selector,arg1,arg2,...);

以上你可能还看不出来它的价值,但是我们需要了解的是OC是一门动态的语言,他会将一些工作放到代码运行时才处理而并非编译时,也就是说,有很多类和成员变量在我们编译时是不知道,而在运行时,我们的代码会转换成完整的确定的代码.
在OC代码中,使用runtime,需要引入#import

2.类在Runtime中的表示

OC类是由Class类型来表示的,他实际上是一个指向objc_class的结构体指针.

typedef struct object_class *Class;

objc_class结构体如下

//类在runtime中的表示
struct objc_class {
    Class isa;//指针,顾名思义,表示是一个什么实例的isa指向类对象,类对象的isa指向元类

#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;
/* Use `Class` instead of `struct objc_class *` */

同样的,我们看看其他的一些相关定义

//描述类中的一个方法
typedef struct objc_method *Method;
//实例变量
typedef struct objc_ivar *Ivar;
// 类别Category
typedef struct objc_category *Category;
// 类中声明的属性
typedef struct objc_property *objc_property_t;

在OC中,一切都被设计成了对象,OC中的类的本质也是一个对象,在runtime中用结构体表示.

3.OC与Runtime的交互

a.OC代码

只需编写OC代码即可,Runtime在幕后搞定一切,编译器会将OC代码转换成运行时代码

b.通过Foundation框架和NSObject类定义方法.

NSObject的一些方法可以从Runtime系统中获取信息

  • -class 返回对象的类;
  • -isKindOfClass 和 - isMemerOfClass 检查对象是否存在于指定的类的继承体系中
  • -respondsToSelector 检查对象能否相应指定的消息
  • -conformsToProtocol 检查对象是否实现了指定的协议
  • -methodForSelector 返回指定方法实现的地址.
c.通过Runtime库objc/runtime.h库函数直接调用.

详细参考后面的实际用法.

4.Runtime的术语

[receiver message];
id objc_msgSend ( id self, SEL op, ... );
1.SEL(实际上是@selector方法取到的类方法的函数地址)

Objc特性-Runtime
SEL和IMP究竟是什么
SEL就是对方法的一种包装。包装的SEL类型数据它对应相应的方法地址,找到方法地址就可以调用方法.
其实它就是映射到方法的C字符串,你可以通过Objc编译器命令@selector()或者Runtime系统的sel_registerName函数来获取一个SEL类型的方法选择器。
如果你知道selector对应的方法名是什么,可以通过NSString* NSStringFromSelector(SEL aSelector)方法将SEL转化为字符串,再用NSLog打印。
它的数据结构是:

typedef struct objc_selector *SEL;

SEL的创建:

SEL s1 = @selector(test1); // 将test1方法包装成SEL对象  
SEL s2 = NSSelectorFromString(@"test1"); // 将一个字符串方法转换成为SEL对象 

一些其他的用法:

// 将SEL对象转换为NSString对象 
NSString *str = NSStringFromSelector(@selector(test)); 
Person *p = [Person new]; 
// 调用对象p的test方法 
[p performSelector:@selector(test)]; 
/* 
 调用方法有两种方式: 
 1.直接通过方法名来调用 
 2.间接的通过SEL数据来调用 
 */ 
Person *person = [[Person alloc] init]; 
    // 1.执行这行代码的时候会把test2包装成SEL类型的数据 
    // 2.然后根据SEL数据找到对应的方法地址(比较耗性能但系统会有缓存) 
    // 3.在根据方法地址调用对应的方法 
[person test1]; 
// 将方法直接包装成SEL数据类型来调用 withObject:传入的参数  
[person performSelector:@selector(test1)]; 
[person performSelector:@selector(test2:) withObject:@"传入参数"]; 
问题来了:为什么不直接用函数指针,而用SEL走一圈再回到函数指针呢?

有了SEL这个中间过程,我们可以对一个编号和什么方法映射做些操作,也就是说我们可以一个SEL指向不同的函数指针,这样就可以完成一个方法名在不同时候执行不同的函数体。另外可以将SEL作为参数传递给不同的类执行。也就是说我们某些业务我们只知道方法名但需要根据不同的情况让不同类执行的时候,SEL可以帮助我们。

2.id

id是通用数据类型,能够表示任何对象

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;

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

3.Class

isa指针的数据类型是Class,Class表示对象所属的类

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

可以查看到Class其实就是一个objc_class结构体指针(如上面2.在runtime中的表示显示的哪有)

4.Method

Method表示类中的某个方法

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

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

其实Method就是一个指向objc_method结构体指针,它存储了方法名(method_name)、方法类型(method_types)和方法实现(method_imp)等信息。而method_imp的数据类型是IMP,它是一个函数指针,后面会重点提及。

5.Ivar

Ivar表示类中的实例变量

/// 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)等信息。

6. IMP

在上面讲Method时就说过,IMP本质上就是一个函数指针,指向方法的实现

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif
当你向某个对象发送一条信息,可以由这个函数指针来指定方法的实现,它最终就会执行那段代码,这样可以绕开消息传递阶段而去执行另一个方法实现。
7.Cache

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


5.Runtime的实际用法.

1.消息发送.

我们来看看,objc_msgSend是如何具体的发送一个消息的:
1.首先根据receiver对象的isa指针获得他的class
2.有现在class的cache查找message方法,如果找不到,再到methodlists查找.
3.如果在class没有找到,再到super_class查找.
4.一旦找到message这个方法,就执行它实现的IMP.

问题,self和super的区别
问题,隐藏参数self和_cmd

(答案在:OC特效:Runtime)

2.方法解析与消息转发

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

Method Resolution

首先Objective-C在运行时调用+ resolveInstanceMethod:或+ resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。

Fast Forwarding

如果目标对象实现- forwardingTargetForSelector:方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding。

Normal Forwarding

如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用methodSignatureForSelector:
方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:
方法。

3.关联对象

OC中的category无法向既有类添加属性,因此可以用runtime的关联对象来实现.

4.Method Swizzling

通过修改一个已存在类的方法,来实现方法替换是比较常用的runtime技巧.

你可能感兴趣的:(iOS知识梳理8:万恶的Runtime)