本文为入门介绍,希望能让第一次接触Runtime概念的朋友有一个概貌了解。
一篇文章,不可能讲完Runtime的全部,但是,分成很多篇讲,又有点「见树木不见森林」的迷糊感觉——自己就是看了很多关于Runtime的文章,看完还是「迷雾重重」(当然,也可能因为资质太过平庸)。
所以,这一篇,尽量涉及。
一句话概括
什么是Runtime?作以下引述。但也不要太奢望看完这些说明后,就会豁然开朗。
官方文档Objective-C Runtime
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C的runtime是一个「运行时库」,为OC这门语言提供动态的特性,所有OC应用程序都与之相关联。
The down low on Objective-C Runtime:
The Objective-C Runtime is an open source library written in C and Assembler that adds the Object Oriented capabilities to C to create the Objective-C language. Objective-C的Runtime,是一个用C和汇编写的「开源库」,它为C添加了面向对象的特性,从而成就了Objrctive-C这门语言。
The Objective-C languages defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language, it’s what makes the language work. Objective-C可以从『编译时』、『链接时』再到『运行时』,hold住尽可能多的决策。只要有可能,它都是动态地干活儿的。这就意味着,这门语言不仅需要一个编译器,还需要一个runtime系统,用来执行编译的代码。这个runtime系统就好比如是Objective-C的「操作系统」,(runtime系统)让这门语言能工作起来。
简单点理解,Runtime就是一个C和汇编写的代码库——是Objective-C之所以成为Objective-C的一个库。
用一图以助理解:
Runtime概览
另外,可参考: 重识 Objective-C Runtime - Smalltalk 与 C 的融合
Runtime的三个头文件
Runtime这个库是开源的。有兴(能)趣(力)的朋友可以仔细研究。
而平时我们会用到的Runtime函数,基本上在runtime.h
, objc.h
, message.h
这三个头文件中。代码2500行 (主要是runtime.h
)
runtime.h
runtime.h中定义了若干「类型(Types)」和「函数(Functions)」。
有我们比较熟悉的Method
,Ivar
,Category
,objc_property_t
,objc_class
类型,都在这里定义。
另外还有106个函数。如常见的:
object_copy()
, class_respondsToSelector()
, class_copyMethodList
等都在这里面。
objc.h
objc.h中定义了Class
, id
, SEL
, IMP
类型。
另外还有6个函数。
message.h
声明了一系列的方法执行函数。
objc_msgSend()
、objc_msgSendSuper()
都定义在这里。
名词解释
isa
isa是一个指针,隐式地存在于实例对象、类中,对象的isa指针指向所属类——因此实例对象能知道自己属于哪个类;类的isa指针指向一个叫「元类(Meta Class))」的玩意儿。
isa
指针在三个地方有定义:
-
objc_class
结构体有声明,指向类的meta类。(在runtime.h
) -
objc_object
结构体有声明,指向对象所属类(在objc.h
) -
NSObject
类有有声明,指向对象所属类;(在NSObject.h
)
Class
Class
定义在objc.h
中第37、38行,是一个指向objc_class
结构体的指针。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
So, Class
就是一个「指针变量」。
而objc_class
结构体在runtime.h
第55-70行中有定义:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;// isa指针, 指向一个meta类,侧面印证:「类也是对象」
#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 *` */
这个结构体,包括:isa指针、父类指针、类名、成员变量、方法列表、缓存以及协议列表等。
解读一下部分成员:
isa指针
上面介绍isa的时候,说过类也有一个isa
指针,我们可以理解为:类本身也是一个对象——「类对象」。是「元类(Meta Class)」的实例(每个类的isa指针指向元类)。
我们熟知的「类方法」,也可以理解为是「类对象」的实例方法。
而这些「元类(Meta Class)」则是「根源类(Root Meta Class)」的实例——所有元类的isa指针最终都指向根元类。根元类的isa指针指向自己,最终完成闭环。
画了一张示意图帮助理解:
isa的指针的指向
struct objc_ivar_list
struct objc_ivar_list
(ivars
),是实例变量列表,保存类所声明的所有实例变量。
objc_method_list
struct objc_method_list
(methodLists
)是方法列表,给某个对象发送消息,就是来这个列表中查找是否有相应方法实现的。
可以动态修改methodLists
的值来添加成员方法,这也是Category的实现原理。
struct objc_cache
struct objc_cache
(cache
),用于缓存方法,调用过的方法会缓存到这里,方便以后索引,提高速度。
Method
定义在runtime.h
第44行,表示一个方法。
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
objc_method结构体存储了方法名、方法类型和方法实现。
SEL
定义在objc.h
第49、50行中,表示一个方法选择器(可以简单点,理解为方法名,一个C语言的字符串)。
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
IMP
定义在objc.h
第54行,表示一个方法的实现。由这个函数指针决定最终执行哪段代码。
/// 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
Method
, SEL
和IMP
有什么区别?
-
Method
:表示一个方法,本质是一个指向objc_method
结构体的指针。 -
SEL
(Selector):在运行时用来代表一个方法的名字。 -
IMP
(Implementation):表示方法的实现部分。第一个参数id指向调用方法的自身,第二个参数是方法的名字seletor,方法的参数紧随其后。
在消息发送的过程中,这三个概念是可以互相转换的。
可以这样理解:
Runtime中,Class维护了一份分发列表(dispatch table),用于消息分发;列表中每个入口,就是一个方法(Method),这份列表的key是selector(SEL),value是implementation(IMP)。
而后面介绍到的Method Swizzling,就是改变这份列表某两个方法的SEL和IMP的对应关系,让seletor对应一个不同的implementation。
(也有人比喻:SLE是门牌号码,IMP是住户)
id
定义在objc.h
第45、46行中,表示一个类的实例对象。
/// A pointer to an instance of a class.
typedef struct objc_object *id;
而objc_object
这个结构体,定义在objc.h
中,这个结构体只有一个指向类的isa指针。
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
Ivar
定义在runtime.h
第44行,表示一个实例变量。
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
Cache
定义在runtime.h
第1841行。
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
Cache的存在,是为方法调用时的性能优化:实例对象收到消息后,会先从Cache中查找,看是否有方法的实现——Runtime会把调用过的方法缓存到Cache中。
objc_property_t
定义在runtime.h
第52,53行。
/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;
表示Objective-C中的属性。
Category
runtime.h
第49,50行:
/// An opaque type that represents a category.
typedef struct objc_category *Category;
objc_category
结构体定义在第1784-1790行。
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
Category可以动态的地为已存在的类添加新的方法。
self & super
先做个实验:
打印:
代码语言:javascript复制NSLog(@"[self class]:%@; [super class]:%@", NSStringFromClass([self class]), NSStringFromClass([super class]));
结果,[self class]
和[super class]
的值是一样的。 Why?不应该打印一个子类,一个父类吗?
self,是一个隐藏参数,隐藏在objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
函数中,发送的所有方法,第一个参数都是self。
而super不是隐藏参数,是一个「编译器标示符」,它告诉编译器,调用父类的方法,而不是本类的方法。但是,这时候实际上的消息的接收者,还是self。
详解解读:
- 执行
[super class]
,会先调用objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
函数; - 再根据
objc_super
结构体的super_class
去查找方法实现,,最后调用objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
执行方法的实现。 所以,最后的接收器还是self。
因此,上述打印结果的值是一样的。
消息的传递流程
关于OC中的消息传递流程,画了一张图以帮助理解(流程由下往上):
消息传递流程
Objective-C的消息传递流程,个人划分为三部分:
- 正常的消息传递(Messaging)
- 消息动态解析(Dynamic Method Resolution)
- 消息转发(Message Forwarding)又分2小步:
- Fast forwarding
- Normal forwarding
第一部分,叫做「正常的消息传递」,那理所当然,后面的就是「不正常」了。事实是:如果能找到方法的实现(IMP/implementation),就不会跳到后面。
Runtime应用
1.获取类的相关情况
比如,我想创建一个类似UITableView的类,然后打算参考一下官方的这个类都声明了哪些方法,可以用以下方式查看(头文件声明的方法并不是全部方法):
代码语言:javascript复制 /* 获取某个类的方法列表(所有方法) */
// 这样获取是实例方法(不是类方法)
Method *methods = class_copyMethodList([UITableView class], &outCount);
for (NSUInteger methodIndex = 0 ; methodIndex < outCount; methodIndex ) {
SEL name = method_getName(methods[methodIndex]);
NSLog(@"Human-例法方实-%@",NSStringFromSelector(name));
}
还有很多其他函数:class_getInstanceVariable()
, objc_getMetaClass()
, class_getClassVariable()
等等。
2.动态添加方法的实现
比如,我们用了某个闭源的框架,不幸地,有个bug是:某方法没有实现,导致crash:
代码语言:javascript复制[Animal jump]: unrecognized selector sent to instance
这时候如果等闭源框架的debug更新,比较被动。而利用Runtime,可以动态地添加方法的实现,防止crash:
代码语言:javascript复制#import "Bird.h"
#import <objc/runtime.h>
// 创建Animal的子类Bird
@implementation Bird
// 如果没有找到实例方法的实现, 就会回调跳到这里
(BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(jump)) {
// 利用Runtime的class_addMethod()函数, 动态添加方法的实现
class_addMethod(self, sel, (IMP)jumpImp, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void jumpImp(id obj, SEL _cmd) {
NSLog(@"执行了jumpImp(动态添加的方法实现)");
}
@end
3.Method Swizzling
Method Swizzling,可以理解为「交换方法的实现(IMP)」,这是网友的说法,官方并没有这种说法,可见苹果官方应该是不提倡这样做的。
假如有个需求:需要记录App每个页面进入的次数(这个需求和Method Swizzling介绍的一样)
我们可以在viewWillAppear:方法中作一些计数处理。但是,每个页面都要写重复的代码。在这里就可以使用Method Swizzling,「动态地」在官方的基础上增加一些代码,以实现需求。
需要新建一个UIViewController的Category,在load方法中实现互换。
代码语言:javascript复制#import "UIViewController Tracking.h"
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
(void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 拿到两个Method对象
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(antony_viewWillAppear:);
Method orignalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod == YES) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(orignalMethod), method_getTypeEncoding(orignalMethod));
}
else {
// 利用method_exchangeImplementations()函数交换两个Method的实现
method_exchangeImplementations(orignalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)antony_viewWillAppear:(BOOL)animated {
// 因为互换了方法, 这里实际调用的是viewWillAppear:的IMP(不会造成递归)
[self antony_viewWillAppear:animated];
// 在这里增加你要的功能
NSLog(@"这是在viewWillAppear:新增的内容");
}
@end
核心就是用method_exchangeImplementations()
函数,互换了viewWillAppear:和antony_viewWillAppear:的实现。
而如果现在创建控制器对象,实际流程是这样的:
-
viewWillAppea:
被执行(实际上执行上述Category的antony_viewWillAppear:
方法) -
antony_viewWillAppear:
方法内又调用了antony_viewWillAppear:
(实际上执行的是系统的viewWillAppear:
方法——因为互换了) - 最后再执行我们自己添加的代码——这样就实现了需求:所有UIViewController在执行
viewWillAppear:
时, 都会调用你增加的代码。从而无须在所有的UIViewController中重复写这部分代码。
Github有个框架:Aspects,就是用Runtime的Method Swizzling实现的,它允许你往任意现存类或实例添加额外的代码。
4.动态添加属性 - 利用Associated Objects(Associative References)
Associative References(关联引用/对象),在runtime.h中定义的三个相关函数:
- objc_setAssociatedObject()
- objc_getAssociatedObject()
- objc_removeAssociatedObjects()
有什么作用呢?
网上有种说法:OC中的Category不能添加属性。
其实严格来说:Category不能添加的是「实例变量」,而属性其实是可以添加的:
- 不能为Category添加实例变量;否则报错:
Instance variables may not be placed in categories
- 但是可以为Category添加属性,也可以自定义setter、getter,外部也可以访问;但是,这个属性是无意义的,因为不能保存数据(可以返回值,但是不能赋值)。而不能保存数据的原因,是因为没有实例变量「装」数据;
而Associated Objects(关联对象),则可以为Category提供保存数据的地方。
因此Associated Objects(关联对象)就可以:给已有类(封闭的类)添加真正有意义的属性——可以保存数据的属性。
比如,我们要为一个叫做Human的类添加一个属性nickName,就可以:
代码语言:javascript复制#import "Human AdditionalProperties.h"
#import <objc/runtime.h>
@implementation Human (AdditionalProperties)
@dynamic nickName;
// 如果要删除该属性,调用objc_setAssociatedObject()赋值为nil即可,
// 不要用objc_removeAssociatedObjects(), 该函数会删除所有添加的属性
- (void)setNickName:(NSString *)nickName {
// 参数1: 为哪个对象实现的关联
// 参数2: 这个关联的key(可以用SEL作为key)
// 参数3: 需要与对应key(参数2)关联的值(就是外部传入的值)
// 参数4: 关联的策略(和属性的attribute相对应)
objc_setAssociatedObject(self, @selector(nickName), nickName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)nickName {
// 参数1: 为哪个对象实现的关联
// 参数2: 该关联的key
return objc_getAssociatedObject(self, @selector(nickName));
}
@end
需要再次强调的是:通过Associated Objects为类添加有意义的属性,事实上并不是添加了实例变量,而是通过关联,使属性有保存数据的能力。(可以用class_copyPropertyList()
验证,并没有增加实例变量。或者断点看该类的实例,并不会看到有添加了实例变量——虽然能用该属性来存取数据。)
5.归档和解档 一键序列化:
有用过NSKeyedArchiver
固化自定义对象到沙盒的朋友应该了解,当一个自定义对象有很多属性,需要一个一个encode(编码)或者decode(解码),是很琐屑的,比如:
自定义类有很多属性
而利用Runtime,则可以简化这个过程——无论类有多少属性:
代码语言:javascript复制- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
unsigned int count = 0;
// 利用class_copyIvarList()拿到类的所有实例变量
Ivar *ivars = class_copyIvarList([self class], &count);
// 再用for循环一次性解档
for (int i = 0; i < count; i ) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int count = 0;
// 利用class_copyIvarList()拿到类的所有实例变量
Ivar *ivars = class_copyIvarList([self class], &count);
// 再用for循环一次性归档
for (int i = 0; i < count; i ) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id object = [self valueForKey:key];
[aCoder encodeObject:object forKey:key];
}
}
Runtime还有很多应用,有兴趣可以继续找相关资料学习。不过:
Objective-C的Runtime就像一把双刃剑,使用它,风险高,回报也高。它赋予你很大的权力,但只要你犯了哪怕一丁点儿错误,都有可能让程序挂掉。
所以,总原则:能不用,尽量不用。
Conclusion
到这里,估计还是有很多黑人问号:Runtime究竟是什么玩意儿? What the hell is Runtime??
这很正常,学习本来就是一个重复的过程——特别是面对学习曲线还比较陡峭的知识。继续实践、温故知新,相信后面会有更好的了解。