文章目录
Runtim官方文档:
Objective-C Runtime Programming Guide
Objective-C Runtime
runtime开源代码
一、Runtime是什么
Runtime又叫运行时,是一套由C、C++、汇编语言编写的API,其为iOS内部的核心之一,我们平时编写的OC代码,底层都是基于它来实现的。
[receiver message];
// 底层运行时会被编译器转化为
objc_msgSend(receiver,selector)
// 如果其还有参数比如:
[receiver message:(id)arg...];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector, arg1, arg2, ...)
二、为什么需要Runtime
- Objective-C 是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时候是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。
- 因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。
- Runtime基本是用C和汇编写的,由此可见苹果为了动态系统的高效而做出的努力。
三、Runtime的作用
- 可以动态的创建、添加类,修改这个类的属性和方法。
- 遍历一个类中的所有成员变量,属性,和方法。
- 用于消息的传递和转发等。
OC在三种层面上与Runtime系统进行交互:
1. 通过Objective-C源代码
只需要编写OC代码,Runtime系统自动在幕后搞定一切,调用方法,编译器会将OC代码转换成运行时代码,在运行时确定数据结构和函数。
2. 通过Foundation框架的NSObject类定义的方法
Cocoa程序中绝大部分类都是NSObject类的子类,所以都继承了NSObject的行为。(NSProxy类是个例外,他是个抽象超类)
一些情况下,NSObject类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如 description 方法,该方法返回类内容得字符串表示,该方法主要用来调试程序。NSObject类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject的子类可以重新实现。
还有一些NSObject的方法可以从Runtime系统中获取信息,允许对象进行自我检查。例如:
class
方法返回对象的类;
isKindOfClass:
和isMemberOfClass:
方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
respondsToSelector:
检查对象是否能响应指定的消息;
conformsToProtocol:
检查对象是否实现了指定协议类的方法;
methodForSelector:
返回指定方法实现的地址。
3. 通过对Runtime库函数的直接调用
Runtime系统是具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下,这意味着我们使用时只需要引入<objc/Runtime.h>
头文件即可。
许多函数可以让你使用纯C代码来实现Objc中同样的功能。除非写一些Objc与其他语言的桥接或是底层的debug工作,你在写Objc代码时一般不会用到这些C语言函数。
四、Runtime的相关术语
1. SEL
它是selector在Objc中的表示(Swift中是Selector类)。selector是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意OC在相同类中不会有命名相同的两个方法。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
方法。
备注:KVO 的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实类型。
3. Class
typedef struct objc_calss *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 *ivar OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *NSCache OBJC2_UNAVAILABLE;
struct objc_property_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;
}
由此可见,我们可以动态修改*methodLists
的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。
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()
会去它元类中查找能够响应消息的方法实现,如果找到了,就会对这个类对象执行方法调用。
最后 objc_class 中还有一个 objc_cache ,缓存,它的作用很重要,后面会提到。
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: 指向了方法的实现,本质是一个函数指针
5. 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
是基地址偏移字节
6. IMP
IMP在objc.h中的定义是:
typedef id (*IMP)(id, SEL, ...);
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。
如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面 Cache 中会提到。
你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id和 SEL 参数就能确定唯一的方法实现地址。
而一个确定的方法也只有唯一的一组 id 和 SEL 参数。
7. 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 一样。
8. 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);
}
打印结果如下:
test[2321:451525] 3
test[2321:451525] name--------T@"NSString",&,N,V_name
test[2321:451525] age--------Ti,N,V_age
test[2321:451525] 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 通过给出属性名在类和协议中获得属性的引用。
五、Runtime消息转发
OC 中的方法调用,编译时候都会转换为 objc_msgSend 函数的调用:
[obj methodName] => objc_msgSend(obj, @selector(methodName))
// 消息接收者:obj
// 消息名称: @selector(methodName)
objc_msgSend 的执行流程可以分为 3 大阶段
- 消息发送
- 找不到消息发送方法,就会进入动态方法解析,允许开发者动态创建新方法;
- 如果动态方法解析没有做任何操作,这时候就开始进入消息转发。
如果这三个阶段都没有搞定,也就是说 objc_msgSend
没找到合适的方法调用,就会报一个很经典的错误:
unrecognized selector sent to instance
关于消息机制的这块的源码,主要是在objc-msg-arm64.s
、objc-runtime-new.mm
以及 Core Foundation 的 forwarding 中(这一块不开源)。
1. 消息发送
下面是消息发送的流程:
2. 动态方法解析
下面是动态方法解析的流程:
我们通过代码去分析一波:
Person.h
@interface Person : NSObject
- (void)test;
@end
Person.m
#import <objc/runtime.h>
@implementation Person
- (void)other {
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test)) {
// 获取 other 方法信息
Method method = class_getInstanceMethod(self, @selector(other));
// 动态添加 test 方法的实现
class_addMethod(self, sel,
method_getImplementation(method),
method_getTypeEncoding(method));
// 返回 YES 代表有动态添加方法
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
在 main 文件运行:
Person *person = [[Person alloc] init];
[person test];
我们通过打印能看到:
[Person other]
我们通过 resolveInstanceMethod 去动态配置 test,当我们运行 test 实际上调用的是 other 方法。上面分析了实例方法,类方法操作也是一样的,只是使用的方法不一样。这里需要注意的是类方法的动态解析中 class_addMethod 第一个参数传的不是 self 而是 object_getClass(self)
。
动态解析过后,会重新走“消息发送”的流程,从 receiverClass 的 cache 中查找方法这一步开始执行。
3. 消息转发
下面是动态方法解析的流程:
还是拿上述的 Person 类举例子:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
// objc_msgSend([[Student alloc] init], aSelector);
return [[Student alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
这时候在 main 调用 Person 的对象方法 test,实际执行的是 Student 的对象方法 test;
如果 forwardingTargetForSelector
返回值是空的;那么就会继续走 methodSignatureForSelector
方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
return nil;
}
return [super forwardingTargetForSelector:aSelector];
}
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
// NSInvocation 封装了一个方法调用,包括:方法调用者、方法名、方法参数
// 方法调用者:anInvocation.target
// 方法名:anInvocation.selector
// 方法参数:[anInvocation getArgument:NULL atIndex:0];
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 等同与 [anInvocation invokeWithTarget:[[Student alloc] init]];
anInvocation.target = [[Student alloc] init];
[anInvocation invoke];
}
开发者可以在 forwardInvocation: 方法中自定义任何逻辑,以上方法都有对象方法、类方法。
4. super
super 调用,底层会转换为 objc_msgSendSuper2 函数的调用,接收2个参数:struct objc_super2、SEL。他直接调用获取父类方法。
这里消息接收者还是子类,只是说从父类开始查找方法实现。
struct objc_super2 {
id receiver; // receiver 是消息接收者
Class current_class; // current_class 是 receiver 的 Class 对象
}
六、Runtime 常见应用场景
- 查看私有成员变量
- 字典转模型
- 替换方法实现
- 给分类增加属性
1. 查看私有成员变量
#import <Foundation/Foundation.h>
@interface Father : NSObject
@property (nonatomic, assign) int age;
@end
#import "Father.h"
@interface Father ()
{
NSString *_name;
}
- (void)sayHello;
@end
@implementation Father
- (id)init
{
if (self = [super init]) {
_name = @"wengzilin";
[_name copy];
self.age = 27;
}
return self;
}
- (void)dealloc
{
[_name release];
_name = nil;
[super dealloc];
}
- (NSString *)description
{
return [NSString stringWithFormat:@"name:%@, age:%d", _name, self.age];
}
- (void)sayHello
{
NSLog(@"%@ says hello to you!", _name);
}
- (void)sayGoodbay
{
NSLog(@"%@ says goodbya to you!", _name);
}
控制变量
- (void)tryMember
{
Father *father = [[Father alloc] init];
NSLog(@"before runtime:%@", [father description]);
unsigned int count = 0;
Ivar *members = class_copyIvarList([Father class], &count);
for (int i = 0 ; i < count; i++) {
Ivar var = members[i];
const char *memberName = ivar_getName(var);
const char *memberType = ivar_getTypeEncoding(var);
NSLog(@"%s----%s", memberName, memberType);
}
}
结果
before runtime:name:wengzilin, age:27
_name----@"NSString"
_age----int
2. 字典转模型
字典转模型重要的两个点:
- 利用 Runtime 遍历所有的属性或者成员变量;
- 利用 KVC 设值。
下面简单实现了一个字典转模型的代码,通过 Runtime 遍历属性列表,并根据属性名取出字典中的对象,然后通过 KVC 进行赋值操作。调用方式和 MJExtension、YYModel 类似,直接通过模型类调用类方法即可。
- (instancetype)initWithDict:(NSDictionary *)dict {
self = [super init];
if (self) {
unsigned int count = 0;
objc_property_t *propertys = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
objc_property_t property = propertys[i];
//通过 property_getName 函数获得属性的名称
const char *name = property_getName(property);
NSString *nameStr = [[NSString alloc] initWithUTF8String:name];
id value = [dict objectForKey:nameStr];
[self setValue:value forKey:nameStr];
}
free(propertys);
}
return self;
}
3. 替换方法实现
替换方法实现常用的两个方法:
// 方法替换
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
// 方法交换
void method_exchangeImplementations(Method m1, Method m2)
例子:
// class_replaceMethod 替换成 imp_implementationWithBlock 中的内容
Person *person = [[Person alloc] init];
class_replaceMethod([Person class], @selector(test), imp_implementationWithBlock(^{
NSlog(@"11");
}), "v");
[person test];
// run 和 test 相互替换
Method runMethod = class_getInstanceMethod([Person class], @selector(run));
Method testMethod = class_getInstanceMethod([Person class], @selector(test));
method_exchangeImplementations(runMethod, testMethod);
4. 给分类增加属性
- 在分类的.h文件中声明想要定义的属性
- 在分类的.m文件中实现getter和setter方法
- 引入runtime头文件,然后在setter方法中用objc_setAssociatedObject关联对象
#import <Foundation/Foundation.h>
@interface NSObject (TempClass)
@property (nonatomic,copy)NSString *name;
@end
#import "NSObject+TempClass.h"
#import <objc/runtime.h>
static void *kName = &kName;
@implementation NSObject (TempClass)
-(void)setName:(NSString *)name
{
// object:给哪个对象添加属性
// key:属性名,根据key去获取关联的对象 ,void * == id
// value:关联的值
// policy:策略
objc_setAssociatedObject(self, kName, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, kName);
}
@end
5. 对象自动归档解档
通过 Runtime 可以获取到对象的 Method List、Property List 等,不只可以用来做字典模型转换,还可以做很多工作。
例如:通过 Runtime 实现自动归档和解档,归档和解档通俗来讲就是将数据写入文件和从文件中读取数据,这一块操作在 iOS 中是需要遵循相对应的协议的。
用 Runtime 提供的函数遍历 Model 自身所有属性,并对属性进行 encode 和 decode 操作。
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
七、Runtime常用API
1. Runtime 关于类的 API
//动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
// 注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)
// 销毁一个类
void objc_disposeClassPair(Class cls)
// 获取isa指向的Class
Class object_getClass(id obj)
// 设置isa指向的Class
Class object_setClass(id obj, Class cls)
// 判断一个OC对象是否为Class
BOOL object_isClass(id obj)
// 判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
// 获取父类
Class class_getSuperclass(Class cls)
2. Runtime 关于成员变量的 API
// 获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)
// 拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
// 设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)
// 动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
// 获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)
3. Runtime 关于属性的 API
// 获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)
// 拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
// 动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)
// 动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)
// 获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)
4. Runtime 关于方法的 API
// 获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)
// 方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)
// 拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
// 动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
// 动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
// 获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)
// 选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)
// 用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)