RunTime的了解与使用
runtime这个词对于iOS程序猿童鞋来说,都是一个“耳熟能详”的名词,因为runtime就像面试中的“诅咒”一样,每当遇到相关面试题,都是几家欢喜几家愁。那么runtime到底是什么呢?
runtime是 OC底层的一套C语言的API,编译器最终都会将OC代码转化为运行时代码。不过苹果已经将 ObjC runtime 代码开源了,我们可以下面的网址浏览源代码:
http://opensource.apple.com/source/objc4/objc4-493.9/runtime/
那么我们就先通过一些经典的面试题来了解一些runtime这个“诅咒”的威力:
1、下面代码输出结果是什么?
@implementation Son : Father
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:(1) Son / Son 因为super为编译器标示符,向super发送的消息被编译成objc_msgSendSuper,但仍以self作为receiver
2、下面代码输出结果是什么?
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
答案:(2) YES / NO / NO / NO <NSObject>协议有一套类方法的隐藏实现,所以编译运行正常;由于NSObject meta class的父类为NSObject class,所以只有第一句为YES
(3) 下面的代码会?Compile Error / Runtime Crash / NSLog…?
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 测试代码
[NSObject foo];
[[NSObject new] foo];
答案(3) 编译运行正常,两行代码都执行-foo。 [NSObject foo]方法查找路线为 NSObject meta class –super-> NSObject class,和第二题知识点很相似。
(4) 下面的代码会?Compile Error / Runtime Crash / NSLog…?
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
NSLog(@"my name's %@", self.name);
}
@end
@implementation ThirdViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
效果.png
答案编译运行正常,输出ThirdViewController中的self对象。 编译运行正常,调用了-speak方法,由于 id cls = [Sark class]; void *obj = &cls; obj已经满足了构成一个objc对象的全部要求(首地址指向ClassObject),所以能够正常走消息机制; 由于这个人造的对象在栈上,而取self.name的操作本质上是self指针在内存向高位地址偏移(32位下一个指针是4字节),按viewDidLoad执行时各个变量入栈顺序从高到底为(self, _cmd, self.class, self, obj)(前两个是方法隐含入参,随后两个为super调用的两个压栈参数),遂栈低地址的obj+4取到了self。
看到上面的几道面试题和答案,有些童鞋可能还是一头雾水,那下面就一点点的捋一捋runtime的功能:
<h2>一:RunTime中的一些名词概念</h2>
objc_msgSend函数定义如下:
id objc_msgSend(id self, SEL op, ...)
<h4>(1)什么是 SEL?</h4>
打开objc.h文件,看下SEL的定义如下:
typedef struct objc_selector *SEL
SEL
是一个指向objc_selector
结构体的指针。而objc_selector
的定义并没有在runtime.h中给出定义。我们可以尝试运行如下代码:
SEL sel = @selector(foo);
NSLog(@"%s", (char *)sel);
NSLog(@"%p", sel);
const char *selName = [@"foo" UTF8String];
SEL sel2 = sel_registerName(selName);
NSLog(@"%s", (char *)sel2);
NSLog(@"%p", sel2);
输出如下:
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
Objective-C在编译时,会根据方法的名字生成一个用来区分这个方法的唯一的一个ID。只要方法名称相同,那么它们的ID就是相同的。
所以两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么它的SEL就是一样的。每一个方法都对应着一个SEL。编译器会根据每个方法的方法名为那个方法生成唯一的SEL。这些SEL组成了一个Set集合,当我们在这个集合中查找某个方法时,只需要去找这个方法对应的SEL即可。而SEL本质是一个字符串,所以直接比较它们的地址即可。
当然,不同的类可以拥有相同的selector。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP
。
<h4>(2)那么什么是IMP
呢?继续看定义:</h4>
typedef id (*IMP)(id, SEL, ...);
IMP本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。
因此我们可以通过SEL
获得它所对应的IMP
,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。
<h4>(3)那么什么是Ivar呢?</h4>
ivar 在objc中被定义为:
typedef struct objc_ivar *Ivar;
它是一个指向objc_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
} OBJC2_UNAVAILABLE;
<h4>(4)@Property</h4>
类中的Property属性被编译器转换成了Ivar,并且自动添加了我们熟悉的Set和Get方法。
<h4>(5)isa</h4>
isa 是一个 objc_class 类型的指针,内存布局以一个 objc_class 指针为开始的所有东东都可以当做一个 object 来对待! 这就是说 objc_class 或者说类其实也可以当做一个 objc_object 对象来对待!这里要区分清楚两个名词:类对象(class object)
与实例对象(instance object)
。ObjC还对类对象
与实例对象
中的 isa 所指向的类结构作了不同的命名:
类对象
中的 isa 指向类结构被称作 metaclass(元类),metaclass 存储类的static类成员变量与static类成员方法(+开头的方法);
实例对象
中的 isa 指向类结构称作 class(普通的),class 结构存储类的普通成员变量与普通成员方法(-开头的方法)。
<h4>(6)super_class:</h4>
一看就明白,指向该类的父类呗!如果该类已经是最顶层的根类(如 NSObject 或 NSProxy),那么 super_class 就为 NULL。
好,先中断一下其他类结构成员的介绍,让我们厘清一下在继承层次中,子类,父类,根类(这些都是普通 class)以及其对应的 metaclass 的 isa 与 super_class 之间关系:
规则一:类的实例对象的 isa 指向该类;该类的 isa 指向该类的 metaclass; 规则二:类的 super_class 指向其父类,如果该类为根类则值为 NULL; 规则三:metaclass 的 isa 指向根 metaclass,如果该 metaclass 是根 metaclass 则指向自身; 规则四:metaclass 的 super_class 指向父 metaclass,如果该 metaclass 是根 metaclass 则指向该 metaclass 对应的类;
<h4>(7)那么 class 与 metaclass 的区别</h4>
class 是 instance object 的类类型。当我们向实例对象发送消息(实例方法)时,我们在该实例对象的 class 结构的 methodlists 中去查找响应的函数,如果没找到匹配的响应函数则在该 class 的父类中的 methodlists 去查找(查找链为上图的中间那一排)。如下面的代码中,向str 实例对象发送 lowercaseString 消息,会在 NSString 类结构的 methodlists 中去查找 lowercaseString 的响应函数。
NSString * str;
[str lowercaseString];
metaclass 是 class object 的类类型。当我们向类对象发送消息(类方法)时,我们在该类对象的 metaclass 结构的 methodlists 中去查找响应的函数,如果没有找到匹配的响应函数则在该 metaclass 的父类中的 methodlists 去查找。如下面的代码中,向 NSString 类对象发送 stringWithString 消息,会在 NSString 的 metaclass 类结构的 methodlists 中去查找 stringWithString 的响应函数。
[NSString stringWithString:@"str"];
<h2>二:Category添加属性</h2>category在我们实际开发过程中,是一个非常实用的得力助手,因为我们可以在不改变原有类的情况下进行方法拓展,但是有些时候,我们也需要为这些category分类添加一些属性供我们使用,问题就在这里,我们都知道:category分类无法直接添加属性,但是我们开发中又有这样的需求,这就尴尬啦~~~
首先我们先看一下,为什么在category中无法直接添加属性呢,category是表示一个指向分类的结构体的指针,其定义如下:
struct objc_category {
char *category_name OBJC2_UNAVAILABLE;// 分类名
char *class_name OBJC2_UNAVAILABLE;// 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE;// 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE;// 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;// 分类所实现的协议列表
}
在这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods
列表是objc_class中方法列表的一个子集,而class_methods
列表是元类方法列表的一个子集。
可发现,类别中没有ivar(ivar代表类中实例变量的类型)
成员变量指针,也就意味着:类别中不能够添加实例变量和属性,struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;// 该类的成员变量链表
简单的理解就是:在category中,系统无法自动为@property
的属性生成 set、get方法
。
【有些人可能就有疑惑了:为什么官方API和一些第三方的框架工具都有为category添加属性的现象呢?比如:NSIndexPath (UITableView)、MJRefresh等等】所以这是我们就需要使用到runtime为category关联一些属性对象。
/**
*通过键值对关联对象
*
* @param object 关联的对象源【通常为 self】
* @param key 唯一键,通过这个键进行取值【const void *是用来起到声明作用】
* @param value 值;“ key ”所对应的值
* @param policy 内存管理策略,枚举:objc_AssociationPolicy
*
* @see objc_setAssociatedObject
* @see objc_removeAssociatedObjects
*/
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
/**
* 通过 key 获取关联值.
*
* @param object 关联的对象源【通常为 self】,在设置关联时所指定的与哪个对象关联的那个对象
* @param key 唯一键,在设置关联时所指定的键
*
* @return 返回唯一键对应的 value 值
*
* @see objc_setAssociatedObject
*/
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
/**
* 取消属性关联对象
*
* @param object 要取消关联属性的对象
*
* @see objc_setAssociatedObject
* @see objc_getAssociatedObject
*/
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
其中的关联策略
也是系统提供的一个枚举:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**表示弱引用关联,通常是基本数据类型 */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**表示强引用关联对象,是线程安全的; 如同:(nonatomic,strong) */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**表示关联对象copy,是线程安全的; 如同:(nonatomic,copy) */
OBJC_ASSOCIATION_RETAIN = 01401, /**表示强引用关联对象,不是线程安全的; 如同:(atomic,strong)*/
OBJC_ASSOCIATION_COPY = 01403 /**表示关联对象copy,不是线程安全的; 如同:(nonatomic,copy) */
};
举个小栗子 ~
.h 中声明
///通过runtime关联数组属性
@property (nonatomic,strong)NSMutableArray * array;
.m 中实现set、get 方法
/// 设置属性关联
-(void)setArray:(NSMutableArray *)array{
objc_setAssociatedObject(self, @selector(array),
array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSMutableArray *)array{
return objc_getAssociatedObject(self, @selector(array));
}
设置调用关联属性.png
set方法被调用.png
get方法被调用.png
当运行程序就可以发现:通过上面的runtime可以完美的为category关联我们需要的属性。
<h2>三:方法交换</h2>
说到方法交换,这个也是在实际开发中经常用到的一个技能,比如:对象默认调用的系统方法API,但是我们又需要对这个方法的调用及实现进行处理的情况下,就可以使用到
runtime
的方法交换method_exchangeImplementations
进行实现这个需求。首先来看一些runtime.h
中提供的几个方法:
/**
* 返回一个指定类对象实现的实例方法。
*
* @param cls 指定的类
* @param name 需要检索的方法
*
*/
OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
/**
* 返回一个指定类对象实现的 类方法。
*
* @param cls 指定的类
* @param name 需要检索的方法
*
*/
OBJC_EXPORT Method class_getClassMethod(Class cls, SEL name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
/**
* 交换两个方法的实现.
*
* @param m1 方法与第二个方法交换。
* @param m2 方法与第一个方法交换。
*
*/
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
举一个小栗子:
对象方法交换.png 从代码运行的效果可以看出:方法交换之后,再调用原来的
eat 方法
,在实现过程中,就会默认进入到交换的 goToSchool 方法
内。这就是关于runtime
方法交换的一个简单示例,在实际开发中可以根据自己的需求进行实际操作啦!<h2>四:动态类型判断</h2>即运行时再决定对象的类型。这类动态特性在日常应用中非常常见,简单说就是id类型。id类型即通用的对象类,任何对象都可以被id指针所指,而在实际使用中,往往使用introspection来确定该对象的实际所属类:
- (BOOL)isMemberOfClass:(Class)aClass
是 NSObject 的方法,用以确定某个 NSObject 对象是否是某个类的成员。- (BOOL)isKindOfClass:(Class)aClass
是用以确定某个对象是不是某个类或其子类的成员;例如:
id obj = someInstance;
if ([obj isKindOfClass:someClass])
{
someClass *classSpecifiedInstance = (someClass *)obj;
// Do Something to classSpecifiedInstance which now is an instance of someClass
//...
}
<h3>五:动态绑定方法</h3>首先先看一下系统提供的动态绑定相关的几个方法:
/**
当这个类被调用了一个没有实现的方法时,会调用到这里
@param sel 未实现的类方法名
*/
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
/**
当这个类被调用了一个没有实现的对象
@param sel 未实现的对象方法名
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
/**
动态添加
class_addMethod([self class], @selector(resolveThisMethodDynamically), (IMP) myMethodIMP, "v@:");
1、Class cls:类的类型
2、name:方法标记 sel
3、imp:方法的实现,是一个 函数指针
4、type:返回值类型
*/
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
/**
上述添加方法中的 IMP imp 所对应的函数格式如下:
*/
void myMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。这里所指的属性和方法当然包括了原来没有在类中实现的,而是在运行时才需要的新加入的实现。在Cocoa层,我们一般向一个NSObject对象发送-respondsToSelector:或者-instancesRespondToSelector:等来确定对象是否可以对某个SEL做出响应,而在OC消息转发机制被触发之前,对应的类的+resolveClassMethod:和+resolveInstanceMethod:将会被调用,在此时有机会动态地向类或者实例添加新的方法,也即类的实现是可以动态绑定的。举个小栗子:
resolveInstanceMethod :
方法,然后我们可以使用class_addMethod
方法进行动态绑定,从而达到预期效果。其中需要注意的是:
performSelector: withObject: 方法
,因为调用这个方法时,系统在编译时是不会进行校验方法是否存在,只有在运行时才会进行查询方法。而直接调用方法时,在编译过程中就会进行校验方法是否存在。<h3>六:获取对象属性</h3>
最典型的用法就是一个对象在
归档 encodeWithCoder
和解档initWithCoder:
方法中需要该对象所有的属性进行encodeObject:
和decodeObjectForKey:
,通过runtime我们声明中无论写多少个属性,都不需要再修改实现中的代码了。
获得某个类的所有成员变量(outCount 会返回成员变量的总数)
参数:
/**
1、哪个类
2、放一个接收值的地址,用来存放属性的个数
3、返回值:存放所有获取到的属性,通过下面两个方法可以调出名字和类型
*/
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
//获得成员变量的名字
const char *ivar_getName(Ivar v)
//获得成员变量的类型(除了基本数据类型)
const char *ivar_getTypeEndcoding(Ivar v)
举个小栗子:
// C语言内 但凡看到 copy creat new 需要释放
// ARC
// 告诉系统归档哪些东西
- (void)encodeWithCoder:(NSCoder *)coder
{
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i ++) {
//取出对应的成员Ivar
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
NSLog(@"成员变量名:%s 成员变量类型:%s",name,type);
NSString * key = [NSString stringWithUTF8String:name];
//归档
[coder encodeObject:[self valueForKey:key] forKey:key];
}
free(ivars);
}
//解档
- (instancetype)initWithCoder:(NSCoder *)coder
{
if (self =[super init]) {
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i ++) {
//取出对应的成员Ivar
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString * key = [NSString stringWithUTF8String:name];
//解档
id value = [coder decodeObjectForKey:key];
//设置到属性身上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
这就是使用runtime
实现的归档解档方法。
<h3>七:字典转模型</h3>
字典转模型这个在实际开发中也是最常用的,因为当我们网络请求到数据后,我们就需要将数据转化为对应的对象模型。不过现在有比较成熟的字典转模型框架,比如:YYModel
、MJExtension
等,那我们如何利用runtime
实现自己的字典转模型框架呢?举个小栗子:
NSObject+Runtime_Model.h 代码逻辑:
@interface NSObject (Runtime_Model)
/** 字典转模型
使用该方法进行字典转模型时,如果使用了设置属性名字与返回的 key 不同时,
需要实现 - (void)setValue:(id)value forUndefinedKey:(NSString *)key 方法,进行转化
*/
+ (instancetype)objectWithDict:(NSDictionary *)dict;
@end
NSObject+Runtime_Model.m 代码逻辑:
#import "NSObject+Runtime_Model.h"
#import <objc/runtime.h>
@implementation NSObject (Runtime_Model)
// 字典转模型
+ (instancetype)objectWithDict:(NSDictionary *)dict{
// 创建对应模型对象
id objc = [[self alloc] init];
// 判断字典中的 key 是否为成员变量,以便为成员变量进行替换赋值
for (NSString * key in dict.allKeys) {
id value = dict[key];
/*判断当前属性是不是Model*/
objc_property_t property = class_getProperty([self class], key.UTF8String);
unsigned int outCount = 0;
objc_property_attribute_t * attributeList = property_copyAttributeList(property, &outCount);
if (!attributeList) {// 属于模型的属性
if ([objc respondsToSelector:@selector(setValue:forUndefinedKey:)]) {
[objc setValue:value forUndefinedKey:key];
}
}
}
unsigned int count = 0;
// 1.获取成员属性数组
Ivar *ivarList = class_copyIvarList(self, &count);
// 2.遍历所有的成员属性名,一个一个去字典中取出对应的value给模型属性赋值
for (int i = 0; i < count; i++) {
// 2.1 获取成员属性
Ivar ivar = ivarList[i];
// 2.2 获取成员属性名 C -> OC 字符串
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 2.3 _成员属性名 => 字典key
NSString *key = [ivarName substringFromIndex:1];
// 2.4 去字典中取出对应value给模型属性赋值
id value = dict[key];
// 属性对应的类名
const char *type = ivar_getTypeEncoding(ivar);
// 获取成员属性类型
NSString *ivarType = [NSString stringWithUTF8String:type];
// 二级转换,字典中还有字典,也需要把对应字典转换成模型
//
// 判断下value,是不是字典
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { // 是字典对象,并且属性名对应类型是自定义类型
// user User
// 处理类型字符串 @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 自定义对象,并且值是字典
// value:user字典 -> User模型
// 获取模型(user)类对象
Class modalClass = NSClassFromString(ivarType);
// 字典转模型
if (modalClass) {
// 字典转模型 user
value = [modalClass objectWithDict:value];
}
// 字典,user
// NSLog(@"%@",key);
}
// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 转换成id类型,就能调用任何对象的方法
id idSelf = self;
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 生成模型
Class classModel = NSClassFromString(ivarType);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel objectWithDict:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
// 2.5 KVC字典转模型
if (value) {
[objc setValue:value forKey:key];
}
}
// 返回对象
return objc;
}
@end
当然这也只是RunTime
的一部分功能,但却是在实际开发中经常用到的知识点。所以如果想要继续拓展RunTime
技能深度的话,可以翻看苹果开源的RunTime
源码http://opensource.apple.com/source/objc4/objc4-493.9/runtime/
参考 :http://devclub.cc/article?articleId=Vn/22T2qcRg1HT4927IyyA==