我们说Objective-C这门语言是一门动态语言,哪个特性来体现的呢,就是runtime运行时系统来体现的。
下面通过几个方面来介绍runtime运行时:
- runtime介绍
- 消息发送
- 消息转发
- runtime使用场景
runtime介绍
先看看官方文档的介绍:
Objective-C语言从编译时间和链接时间到运行时推迟了尽可能多的决策。只要有可能,它就会动态地完成任务。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译代码。运行时系统充当Objective-C语言的一种操作系统;这就是语言运作的原因。
Objective-C runtime运行时有两个版本,lagecy(遗产)和modern(现代),modern版本运行时随Objective-C2.0推出,Objective-C2.02.0包含了许多新功能,其中一点是当更改类的实例变量布局时,不必重新编译。
运行时系统的核心是消息传递和转发,以及在消息传递和转发中动态的加载类并查找有关对象的信息。
不管你是显式的调用了runtime的API还是直接写OC有关的代码,编译器都会生成相应的数据结构和函数。
runtime运行时系统是一个动态共享库,可以使用 #<objc/runtime>来查看内部的API及各种结构体定义,且runtime是开源的,你可以在这里下载。
消息发送
曾经有这么一个面试题:
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"class = %@",NSStringFromClass([self class]));
NSLog(@"class = %@",NSStringFromClass([super class]));
}
return self;
}
打印:
class = Person
class = Person
自己先想想为啥...
接着看:
举个例子:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)test {
NSLog(@"%@",NSStringFromSelector(_cmd));
}
@end
我们经常说在viewDidLoad方法里通过self调用test方法,[self test],其实确切的应该说成给 self 发送 test 消息 当编译完后该代码转换成了如下代码:
objc_msgSend(self,sel_registerName("test"));
需要设置下xcode,不让xcode检查是否使用objc_msgSend发送消息,要不然会报错;
项目target--build Setting --搜索objc_msgSend ---设置为NO即可
objc_msgSend定义如下:
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);
在想弄清楚runtime是怎么完成消息发送前,我们先来看下,OC对象和类在runtime中的定义:
/// An opaque type that represents an Objective-C class. 类的定义
typedef struct objc_class *Class;
/// Represents an instance of a class.代表类的一个实例
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
//类结构体
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;//指向元类
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;//指向父类(类也是一个对象)
const char * _Nonnull name OBJC2_UNAVAILABLE;//name
long version OBJC2_UNAVAILABLE;//版本
long info OBJC2_UNAVAILABLE;//info
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;
看完上边的实例及类的定义,加上我们的开发经验,其实已经知道消息发送的一个流程了:
- 通过实例self的isa指针找到当前所属类
- 在当前类的objc_cache中查找,有没有test方法
- cache中没有找到test方法,再去methodList中去查找,找到就执行IMP,没有找到通过isa指针去父类中去找,直到找到,找不到就报unrecognized selector xxx
方法查找首先去objc_cache中查找是为了提高效率,调用过的方法会存放在缓存中,不能每次调用一个方法都要查找一遍methodList,显然是不行的。缓存的时候会把test的method_name作为key,方法的method_imp实现作为value来存储。
其他的一些结构体声明:
- id
- SEL
- Method
- Ivar
- Category
- objc_property_t
- objc_cache
id
id类型,就是一个objc_object结构体指针,指向任意对象
/// A pointer to an instance of a class.
typedef struct objc_object *id;
SEL
方法选择器,可以理解为方法ID,可以联想到OC语言一个类中方法不能重载
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
Method
一个方法有方法名,有types,有方法实现;types举例:@"v@:" v代表返回值为void,@ 表示参数为参数为对象类型,: 表示selector方法选择器 具体请移步这里。
struct objc_method {
SEL _Nonnull method_name //方法名 OBJC2_UNAVAILABLE;
char * _Nullable method_types //types(@"v@:") OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp //方法实现IMP OBJC2_UNAVAILABLE;
}
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
}
Category类别
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;//协议列表
}
objc_property_t 属性
/// An opaque type that represents an Objective-C declared property.类声明的属性
typedef struct objc_property *objc_property_t;
//获取一个类中的属性列表
objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount);
objc_cache
缓存,刚才讲过了,寻找类的方法先是去缓存中找的,内部有个 buckets实例变量,意思很明白,一个方法桶子
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;
};
以上简单列举了下,runtime运行时系统其他的结构体声明,理解了这些在加上内部的函数,就能更灵活的运用到自己的项目当中,内部更具体的函数、结构体声明请移步官方文档。
消息转发
我们都知道,假如你调用了一个没有实现的方法,会报unrecognised selector xxx,其实即便你没有实现这个方法,通过runtime的消息转发机制,你仍然还有机会让你的程序不崩溃。
NSObject类中有这么几个方法,以下几个方法就是运行时系统在给你最后机会来自己处理消息转发:
1、
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
2、- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
3、
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
挂
- (void)doesNotRecognizeSelector:(SEL)aSelector;
可以概括为三种方式:
- 动态的添加方法实现,返回YES
- 返回实现该方法的对象
- 通过返回NSInvocation和MethodSignature完成一次完整转发
动态添加方法实现
举例:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
objc_msgSend(self,sel_registerName("test"));
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"test")) {
BOOL added = class_addMethod([self class], NSSelectorFromString(@"test"), (IMP)testImp, "v@:");
if (added) {
return YES;
}
return NO;
}
//其他系统访问返回super调用
return [super resolveInstanceMethod:sel];
}
void testImp(id self,SEL _cmd) {
NSLog(@"method name = %@",NSStringFromSelector(_cmd));
}
@end
打印:
method name = test
来看下官方文档的说明:
Dynamically provides an implementation for a given selector for an instance method.动态提供一个给定实例的方法实现,说的很明白了。
一个OC方法默认有两个参数(self 和 SEL),使用class_addMethod函数来动态添加一个函数实现,并返回YES
返回实现该方法的对象
如果你实现了forwardingTargetForSelector这个方法,系统就会调用它,会去你返回的那个对象实例中寻找方法,然后调用。
现在我创建了一个Person类,里边声明了test方法并实现。
@interface Person : NSObject
- (void)test;
@end
@implementation Person
- (void)test {
NSLog(@"class_name = %@ , method_name = %@",[self class],NSStringFromSelector(_cmd));
}
@end
VC内部:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
objc_msgSend(self,sel_registerName("test"));
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"test")) {
return NO;
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == NSSelectorFromString(@"test")) {
return [Person new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
打印:
class_name = Person , method_name = test
系统首先调用resolveInstanceMethod方法,发现没有找到方法实现,就会调用方法forwardingTargetForSelector,该方法内部返回Person对象实例,Person对象内部实现了test方法,然后调用了test方法。
看下系统给的解释:
如果对象实现(或继承)此方法,并返回非零(和非自身)结果,则返回的对象将用作新的接收器对象,并且消息调度将恢复到该新对象,如果这个方法返回self,会直接崩溃。
也就是系统会重新进行一次消息转发,消息的接收者变成了该方法返回的对象。
通过返回NSInvocation和MethodSignature完成一次完整转发
如果以上两个机会你都没有把握,系统就会开启一次完整的消息转发,步骤如下:
- 通过实现methodSignatureForSelector方法,实例化一个方法签名对象NSMethodSignature返回,这个方法签名对象是对方法method的一个描述
- runtime动态生成一个NSInvocation对象传递给forwardInvocation方法
- 通过实现forwardInvocation来指定一个对象,通过这个对象完成消息转发
看下面代码:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
objc_msgSend(self,sel_registerName("test"));
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sig;
}
return [super methodSignatureForSelector:aSelector];
}
//这个官方有示例
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
Person *p = [[Person alloc] init];
if ([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
} else {
[super forwardInvocation:anInvocation];
}
}
@end
打印:
class_name = Person , method_name = test
有关NSMethodSignature和NSInvocation类,内部很简单,可以点进去看下。
以上就是消息转发的一个流程。
runtime使用场景
结合自己项目中runtime使用场景来列举下:
- 对象绑定
- 方法交换
- 自定义对象归档返归档
- dic-model
- KVO自定义实现
1. 对象绑定
直接看例子:
@interface Person : NSObject
- (void)bind;
@end
NSString * const personSource = @"personSource";
@implementation Person
- (NSString *)description {
NSString *str = objc_getAssociatedObject(self, &personSource);
return [NSString stringWithFormat:@"%@-刚出生就拥有:%@",self.name,str];
}
- (void)bind {
objc_setAssociatedObject(self, &personSource, @"北京二环两套房+500万", OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (void)dealloc {
objc_removeAssociatedObjects(self);
}
//VC中
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
p.name = @"小明";
[p bind];
NSLog(@"%@",p);
}
打印:
小明-刚出生就拥有:北京二环两套房+500万
@end
通过上边这个例子可以看出:现在是拼爹的时代,哈哈。
看下绑定对象API:
/**
* 通过给定key和绑定策略将value绑定到object上!
*
* @param object 被绑定的对象
* @param key 绑定value使用的key,注意:key-value一一对应
* @param value 绑定的value,传空表示清空value
* @param policy 绑定策略,用于绑定对象的内存管理
* @see objc_setAssociatedObject
* @see objc_removeAssociatedObjects
*/
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
/**
* 根据key返回对象绑定的value值
* @param object 被绑定的对象
* @param key key
* @return 绑定到对象上的value值
* @see objc_setAssociatedObject
*/
id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
//清除所有绑定到对象上的value,一般不用这个方法,使用objc_setAssociatedObject方法,value传递nil即可
void objc_removeAssociatedObjects(id _Nonnull object)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
绑定策略:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0,//弱引用+原子性
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,//强引用+非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,//copy+非原子性
OBJC_ASSOCIATION_RETAIN = 01401, //强引用+原子性
OBJC_ASSOCIATION_COPY = 01403 //copy+原子性
};
缓存策略用于绑定对象value内存管理用的,跟常见的属性修饰符管理完全相同。
2. 方法交换
方法交换,核心就是交换方法的实现(IMP),有图为证
屏幕快照 2019-01-10 18.33.39.png
简单说一下核心方法实现:
- 根据旧类class和SEL,获取要被替换的Method,即originMethod
- 根据新类class和SEL获取新加的Method,即cusMethod
- 如果没有originMethod,直接向旧类添加新方法,添加成功,就替换老方法的实现(IMP)
- 直接交换方法实现
void tt_swizzleMethodImplementation(Class originC,Class cusC ,SEL originSEL, SEL cusSEL) {
Method originMethod = class_getInstanceMethod(originC, originSEL);
Method cusMethod = class_getInstanceMethod(cusC, cusSEL);
if (!originMethod) {
BOOL added = class_addMethod(originC, cusSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
if (added) {
class_replaceMethod(originC, originSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
}
} else {
method_exchangeImplementations(originMethod, cusMethod);
}
}
这个我前一阵写过一个例子,就是通过方法交换来优化appDelegate内部的推送部分代码的,优化后你会发现,appDelegate推送部分代码非常简洁,里边写的比较详细,有需要的可以看下推送优化传送门,方法交换就此别过。
3. 自定义对象归档返归档
当存储自定义数据(model)时,需要用到归档反归档。
系统对归档的内部实现猜测是以key-value键值对存储的。通过实现系统的两个协议来告知系统你要存储的数据。需要实现NSCoding协议内的两个方法
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;
一般如果model属性少的话,手写还是可以的,但是属性过多还要手写? 通过runtime来动态遍历出model的属性key,然后来赋值或取值。MJExtension内部有实现,而且一个宏搞定。
看下核心代码的实现:
- (void)tt_decode:(NSCoder *)decoder {
unsigned int count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
objc_property_t property = properties[i];
const char *name = property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:name];
NSString *propertyValue = [decoder decodeObjectForKey:propertyName];
if (nil != propertyValue) {
[self setValue:propertyValue forKey:propertyName];
}
}
free(properties);
}
- (void)tt_encode:(NSCoder *)encoder {
unsigned int count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
objc_property_t property = properties[i];
const char *name = property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:name];
id value = [self valueForKey:propertyName];
if (nil != value) {
[encoder encodeObject:value forKey:propertyName];
}
}
free(properties);
}
这块不多说,需要注意的是最后需要释放objc_property_t指针,因为这是在OC环境,对C语言不自动管理内存。
dic-model(字典转模型)
字典转模型也不多说,看看常用的开源框架就行,核心是遍历出对象的属性列表然后把以属性名(可以自定义)为key从字典取出对应的value赋值给对象属性。
KVO自定义实现
当你了解了KVO的底层原理后,也可以尝试着使用runtime运行时来写一个自己的KVO,当然有可能会有bug,即便自己实现一个KVO也没必要用到项目中,毕竟系统的或RAC很成熟了,自己写的目的其实很简单,通过自己实现可以更熟悉底层的实现,从而给自己带来一些思考。前一阵自己实现过一个,也有demo,有需要的自己去看吧,这里不多说。
面试题
回过头来看看那个面试题,我们知道[self class] 等价于
objc_msgSend(self, @selector(class));
那么[supser class]等价于
objc_msgSend(self, @selector(class));
struct objc_super superClass = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
objc_msgSendSuper(&superClass, _cmd);
通过上面转化后的代码我们就清楚了,消息的接收者都是self,super只是一个标识符而已。
以上就是runtime运行时的一个概括汇总吧,只有当你真正理解了runtime,才会领悟这门语言动态的含义。
奋然前行!
点赞是你的态度!
网友评论