IOS 开发— Runtime 机制
Runtime 底层原理与应用
Runtime 又叫运行时,是一套IOS底层的C语言API。Objective-C在运行时被转化为C语言的API。Objective-C时一门动态语言,在代码运行才进行处理和编译。因此需要一个运行时系统(Runtime System)来处理编译后的Code。
Runtime 基本是用C和汇编实现的,苹果和GNU各自维护一套Runtime版本,两个版本之间Apple也在努力保持一致。Runtime 源码下载
Runtime源码分析
源码中提供了Runtime的一些参数和接口和一些专业的术语,他们对应着的数据结构如下:
SEL
他是Objective-C 中selector方法选择器在 Runtime中的表示。PS:Objective-C在相同的类中不会有命名相同的两个方法,Selector对方法名进行包装方便找到对应的方法。
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
映射到方法的名的字符串,通过Objective-C 编译器命令@selector()
或者Runtime 系统的sel_registerName
函数来获取一个SEL类型的方法选择器。PS:不同类中的相同方法所对应的 SEL是相同的,由于变量的类型不同,所以回不导致方法实现混乱。
id
Id 是一个参数类型,指向某个类的实力指针:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
可以根据objc_object结构体中的isa 指针来找到对象所属的类。PS:isa 指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的 -class
方法。KVO 的实现机制就是将被观察者对象的isa指针指向一个中间类而非一个真实的类型。
Calss
指向objc_class结构体指针。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
可以看出objc_class中关联类 父类的指针、类名、成员变量、方法、缓存和协议。
objc_ivar_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;
}
objc_method_list 方法列表
// 方法列表
struct objc_method_list {
struct objc_method_list * _Nullable 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;
} OBJC2_UNAVAILABLE;
Method
Method 代表类中某个方法的类型
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
objc_method
存储了方法名,方法类型和方法实现:
- 方法名类型为
SEL
- 方法类型
method_types
是个 char 指针,存储方法的参数类型和返回值类型 method_imp
指向了方法的实现,本质是一个函数指针
Ivar
Ivar
是表示成员变量的类型。
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
其中ivar——offset是基地址偏移字节。
IMP
/// 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
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP
这个函数指针就指向了这个方法的实现。
如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面 Cache
中会提到。
你会发现 IMP
指向的方法与 objc_msgSend
函数类型相同,参数都包含 id
和 SEL
类型。每个方法名都对应一个 SEL
类型的方法选择器,而每个实例对象中的 SEL
对应的方法实现肯定是唯一的,通过一组 id
和 SEL
参数就能确定唯一的方法实现地址。
而一个确定的方法也只有唯一的一组 id
和 SEL
参数。
Cache
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
Cache 为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在 isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。
Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache 一样。
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)
Ps: 返回的是属性列表,列表中每个元素都是一个 objc_property_t
指针。
@interface CPerson : NSObject
@property(strong,nonatomic) NSString *m_Name;
@property(assign,nonatomic) unsigned int n_Age;
@property(assign,nonatomic) double d_Weight;
@end
-(void)Runtime
{
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList([CPerson class], &outCount);
NSLog(@"OutCount: %d",outCount);
for (unsigned int i =0; i<outCount; i++)
{
NSString *name = @(property_getName(properties[i]));
NSString *attributes = @(property_getAttributes(properties[i]));
NSLog(@"%@ : ----- :%@",name,attributes);
}
NSLog(@"Class Name: %@",NSStringFromClass([super class]));
}
Log 输出结果:
2019-01-22 17:04:06 ObjcDemo[6758:322755] OutCount: 3
2019-01-22 17:04:06 ObjcDemo[6758:322755] m_Name : ----- :T@"NSString",&,N,V_m_Name
2019-01-22 17:04:06 ObjcDemo[6758:322755] n_Age : ----- :TI,N,V_n_Age
2019-01-22 17:04:06 ObjcDemo[6758:322755] d_Weight : ----- :Td,N,V_d_Weight
OC方法底层调用与探索
Objective-C在三个层面上与 Runtime 系统进行交互
1、通过Objective-C源代码。
2、通过Foundation框架的NSObject 类定义的方法。
3、通过对Runtime库函数直接调用
Objective-C 源代码
我们所编写的Objective-C代码,在运行时,系统会自动转化为运行时代码,在运行时确定运行数据和函数。
NSObject类定义的方法
我们所写的类大部分是NSObject 类的子类(NSProxy 类时个例外,它是个抽象超类)。
NSObject 的一些方法可以从Runtime 系统获取信息 例如:
-class
返回对象类。
-isKindOfClass
和 -isMemberOfClass
检查对象是否存在指定类的继承体系中。
-respondsToSelector
检查能否响应指定的消息。
-conformsToProtocol
检查是否实现了指定协议的方法。
-methodForSelector
返回指定方法的实现地址。
Runtime库函数调用
Runtime 头文件位置/usr/include/objc
目录下面,因此我们可以直接引入objc/Runtime.h
头文件来直接调用。
OC消息发送机制
苹果官方文档中的 messages aren’t bound to method implementations until Runtime。消息直到运行时才会与方法实现进行绑定。
objc_msgSend
方法看清来好像返回了数据,其实objc_msgSend
从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。下面详细叙述消息发送的步骤(如下图):
流程:
1、先检查这个selector 是不是要忽略。比如Mac OS X开发,有了垃圾回收旧不会理会 retain、release等函数。
2、检测这个selector 的target 是不是nil,Objective-C允许对一个nil对象执行任何方法而不会Crash,因为运行时会被忽略掉。
3、如果上面两个都通过了,那么就开始检查这个类的实现IMP,先从cache里面查找,如果找到啦就会运行对应的函数区执行相应的代码。
4、如果cache找不到就找类的方法列表中是否有对应的方法。
5、如果类的方法中找不到就到父类的方法列表中去查找,一直到NSObject类为止。
6、如果找不到,就要开始进行动态方法解析。
在消息传递的过程中,编译器会根据objc_msgSend、objc_msgSend_stret、objc_msgSendSuper、objc_msgSendSuper_stret这四个方法中一个调用,如果方法传递给父类,那么就会调用夫类的Super函数,如果消息返回值是结构体而不是简单的值时,会调用名称带有stret的函数。PS:方法中隐藏参数self(接收消息的对象)、方法选择器(SEL指针)。
获取方法地址
NSObject
类中有一个实例方法:methodForSelector
,你可以用它来获取某个方法选择器对应的 IMP
。
OC动态方法解析
你可以动态提供一个方法实现。如果我们使用关键字 @dynamic
在类的实现文件中修饰一个属性,表明我们会为这个属性动态提供存取方法,编译器不会再默认为我们生成这个属性的 setter 和 getter 方法了,需要我们自己提供。
@dynamic propertyName;
当 Runtime 系统在 Cache 和类的方法列表(包括父类)中找不到要执行的方法时,Runtime 会调用 resolveInstanceMethod:
或 resolveClassMethod:
来给我们一次动态添加方法实现的机会。我们需要用 class_addMethod
函数完成向特定类添加特定方法实现的操作:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
上面的例子为 resolveThisMethodDynamically
方法添加了实现内容,就是 dynamicMethodIMP
方法中的代码。其中 "v@:"
表示返回值和参数,这个符号表示的含义见:Type Encoding
OC消息转发机制
重定向
消息转发机制执行前,Runtime 系统允许我们替换消息的接收者为其他对象。通过 - (id)forwardingTargetForSelector:(SEL)aSelector
方法。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
如果此方法返回 nil
或者 self
,则会计入消息转发机制(forwardInvocation:
),否则将向返回的对象重新发送消息。
转发
当动态方法解析不做处理返回 NO
时,则会触发消息转发机制。这时 forwardInvocation:
方法会被执行,我们可以重写这个方法来自定义我们的转发逻辑:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
唯一参数是个 NSInvocation
类型的对象,该对象封装了原始的消息和消息的参数。我们可以实现 forwardInvocation:
方法来对不能处理的消息做一些处理。也可以将消息转发给其他对象处理,而不抛出错误。PS: 在 forwardInvocation:
消息发送前,Runtime 系统会向对象发送methodSignatureForSelector:
消息,并取到返回的方法签名用于生成 NSInvocation 对象。所以重写 forwardInvocation:
的同时也要重写 methodSignatureForSelector:
方法,否则会抛异常。
Category
#import "UIImage+Image.h"
#include <objc/runtime.h>
@implementation UIImage (Image)
+(void)load
{
Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
Method xmg_ImageNameMethod = class_getClassMethod(self, @selector(xmg_ImageName:));
method_exchangeImplementations(imageNameMethod, xmg_ImageNameMethod);
}
+(UIImage *)xmg_ImageName:(NSString *)name
{
UIImage *image = [UIImage xmg_ImageName:name];
if(image)
NSLog(@"Success !");
else
NSLog(@"Faile !");
return image;
}
@end