以前在学习Runtime,概念性东西太多,总是有种朦胧的感觉。现在潜心研究Runtime。发现越深入了解,越能接触到很多深入底层的东西,有种学无止境的感觉。
Runtime是一套比较底层的纯C语言的API, 属于C语言库, 包含了很多底层的C语言API。 在我们平时编写的iOS代码中, 最终都是转成了Runtime的C语言代码。所谓运行时,也就是在编译时是不存在的,只是在运行过程中才去确定对象的类型、方法等。利用Runtime机制可以在程序运行时动态修改类、对象中的所有属性、方法等。譬如C++也是运行时动态的识别类的对象,简称对象的动态绑定等。
相信我们都遇到过这样一个问题:我想在扩展(category)中添加一个属性,iOS是不允许给扩展类扩展属性的,那怎么办呢?答案就是使用Runtime机制和关联属性。还有就是我们在网络请求数据处理时,调用了-setValuesForKeysWithDictionary:方法来设置模型值。这里其实也是通过Runtime机制来Finish,内部会遍历模型类的所有属性名,然后设置与key对应的属性名的值。在使用Runtime前,先要包含头文件:#import <objc/runtime.h>。如果是Swift就不需要包含头文件,就可以直接使用了。
1.消息(Message)
为什么叫消息呢?因为面向对象编程中,对象调用方法叫做发送消息。在编译时,应用的源代码就会被编将对象发送消息转换成runtime的objc_msgSend函数调用。
在Objective-C,消息在运行时并不要求实现。编译器会转换消息表达式:
[receiver message];
在编译时会转换成类似这样的函数调用:
objc_msgSend(receiver, selector);
或
((void (*)(id, SEL, id))objc_msgSend)(self, @selector(method), param)
具体会转换成哪个,我们来看看官方的原话:
When it encounters a method call, the compiler generates a call to one of the
* functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
* Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper;
* other messages are sent using \c objc_msgSend. Methods that have data structures as return values
* are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
也就是说,我们是通过编译器来自动转换成运行时代码时,它会根据类型自动转换成下面的其它一个函数:
objc_msgSend:其它普通的消息都会通过该函数来发送
objcmsgSendstret:消息中需要有数据结构作为返回值时,会通过该函数来发送消息并接收返回值
objcmsgSendSuper:与objcmsgSend函数类似,只是它把消息发送给父类实例
objcmsgSendSuperstret:与objcmsgSendstret函数类似,只是它把消息发送给父类实例并接收数组结构作为返回值
另外,如果函数返回值是浮点类型,官方说明如下:
* arm: objc_msgSend_fpret not used
* i386: objc_msgSend_fpret used for `float`, `double`, `long double`.
* x86-64: objc_msgSend_fpret used for `long double`.
*
* arm: objc_msgSend_fp2ret not used
* i386: objc_msgSend_fp2ret not used
* x86-64: objc_msgSend_fp2ret used for `_Complex long double`.
其实这是一个条件编译,我们不用担心是哪种处理器上,我们只需要调用objc_msgSend_fpret函数即可。
当消息被发送到实例对象时,它是如何处理的:
image
我们的根类是NSObject,它会一层一层的传递,直接找到要处理该消息的对象,若都没有找到,正常情况下会出现Unreconized selector ...这样的崩溃提示了。
1.1 Message Forwarding
当发送消息给一个不处理该消息的对象是错误的。然后在宣布错误之前,运行时系统给了接收消息的对象处理消息的第二个机会。
当某对象不处理某消息时,可以通过重写-forwardInvocation:方法来提供一个默认的消息响应或者避免出错。当对象中找不到方法实现时,会按照类继承关系一层层往上找。我们看看类继承关系图:
所有元类中的isa指针都指向根元类,而根元类的isa指针则指向自身。根元类是继承于根类的,与根类的结构体成员一致,都是objc_class结构体,不同的是根元类的isa指针指向自身,而根类的isa指针为nil
我们再看看消息处理流程:
当对象查询不到相关的方法,消息得不到该对象处理,会启动“消息转发”机制。消息转发还分为几个阶段:先询问receiver或者说是它所属的类是否能动态添加方法,以处理当前这个消息,这叫做“动态方法解析”,runtime会通过+resolveInstanceMethod:判断能否处理。如果runtime完成动态添加方法的询问之后,receiver仍然无法正常响应则Runtime会继续向receiver询问是否有其它对象即其它receiver能处理这条消息,若返回能够处理的对象,Runtime会把消息转给返回的对象,消息转发流程也就结束。若无对象返回,Runtime会把消息有关的全部细节都封装到NSInvocation对象中,再给receiver最后一次机会,令其设法解决当前还未处理的这条消息。
消息处理越往后,开销也就会越大,因此最好直接在第一步就可以得到消息处理。
我们看看类结构体:
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 *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
我们可以看到每个类结构体都会有一个isa指针,它是指向元类的。它还有一个父类指针super_class,指针父类。包含了类的名称name、类的版本信息version、类的一些标识信息info、实例大小instance_size、成员变量地址列表ivars、方法地址列表methodLists、缓存最近使用的方法地址cache、协议列表protocols`。
我们在使用时,经常使用到Class,它就是:
typedef struct objc_class *Class;
当类为根类时,它的super_class就会是nil。普通的Class存储的是实例成员,如-号方法、属性、成员变量,而isa则指向元类,而元类存储的是静态成员,如+号方法、static成员。
2.我们可以利用Runtime机制来实现类似拷贝copy,mutableCopy功能,那就是使用object_copy来实现对象拷贝。object_copy方法,随之对应的方法是object_dispose方法,不过这两个方法在ARC下无法通过编译器,OBJC_ARC_UNAVAILABLE。
API:id object_copy(id obj, size_t size)和id object_dispose(id obj)。
3.class_addMethod 一般用在消息自动解析时动态新增方法来进行消息的响应,从而达到消息发送的目的。
动态方法解析
你可以动态地提供一个方法的实现。例如我们可以用@dynamic关键字在类的实现文件中修饰一个属性:
@dynamic propertyName;
这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:和propertyName方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:和resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当 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。
参数解析:
BOOL class_addMethod(Class cls,SEL name,IMP imp, const char *types)
Class 可以理解为要添加方法的目标类的名称
SEL name 可以理解为 要添加的方法名
IMP imp 这个可以理解为实现的方法名称,至少要包含2个参数,分别是 id self, SEL _cmd
Const char *types 可以理解为参数的属性。
其中types参数为"i@:@“,按顺序分别表示:
i 表示 返回值类型int,若是v则表示void
@ 表示 参数id(self)
: 表示 SEL(_cmd)
@ 表示 id(str)
Type Encoding
编码值 含意
c 代表char类型
i 代表int类型
s 代表short类型
l 代表long类型,在64位处理器上也是按照32位处理
q 代表long long类型
C 代表unsigned char类型
I 代表unsigned int类型
S 代表unsigned short类型
L 代表unsigned long类型
Q 代表unsigned long long类型
f 代表float类型
d 代表double类型
B 代表C++中的bool或者C99中的_Bool
v 代表void类型
* 代表char *类型
@ 代表对象类型
# 代表类对象 (Class)
: 代表方法selector (SEL)
[array type] 代表array
{name=type…} 代表结构体
(name=type…) 代表union
bnum A bit field of num bits
type A pointer to type
? An unknown type (among other things, this code is used for function pointers)
我们想要通过运行时处理各种类型,那么我们必须要知道哪种字符代表什么类型。
4.Method Swizzling
这个是Swizzling技术,可以通过它进行"骗取"系统(其实就是调换方法,重新映射方法对应的实现),达到"偷天换日"的效果。
#pragma mark - method swizzling
- (void)methodExchange {
Method m1 = class_getInstanceMethod([NSString class],@selector(lowercaseString));
Method m2 = class_getInstanceMethod([NSString class],@selector(uppercaseString));
method_exchangeImplementations(m1, m2);
NSLog(@"%@", [@"helloWorld" lowercaseString]);
NSLog(@"%@", [@"heLLoworLd" uppercaseString]);
}
运行结果:
2015-01-08 11:02:36.937 rumtimeDemo[1367:681073] HELLOWORLD
2015-01-08 11:02:36.937 rumtimeDemo[1367:681073] helloworld
举个摘自老外博客的例子
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
5.获取对象所有属性名
利用runtime获取对象的所有属性名是可以的,但是变量名获取就得用另外的方法了。我们可以通过class_copyPropertyList方法获取所有的属性名称。
- (void)getClassAllPropertyList
{
u_int count;
objc_property_t properties=class_copyPropertyList([UIViewController class], &count);
for (int i = 0; i < count ; i++)
{
const char propertyName =property_getName(properties[i]);
NSString *strName = [NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding];
NSLog(@"propertyName:%@",strName);
}
// 注意,这里properties是一个数组指针,就是二级指针的意思,是C的语法,
// 我们需要使用free函数来释放内存,否则会造成内存泄露
free(properties);
}
6.获取对象的成员变量名称
要获取对象的成员变量,可以通过class_copyIvarList方法来获取,通过ivar_getName来获取成员变量的名称。对于属性,会自动生成一个成员变量。
- (NSArray *)allMemberVariables {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
NSMutableArray *results = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < count; ++i) {
Ivar variable = ivars[i];
const char *name = ivar_getName(variable);
NSString *varName = [NSString stringWithUTF8String:name];
[results addObject:varName];
}
return results;
}
7.获取类的所有方法
- (void)getClassAllMethod
{
u_int count;
Method* methods= class_copyMethodList([UIViewController class], &count);
for (int i = 0; i < count ; i++)
{
SEL name = method_getName(methods[i]);
NSString *strName = [NSString stringWithCString:sel_getName(name)encoding:NSUTF8StringEncoding];
NSLog(@"methodName:%@",strName);
}
}
8.Category扩展属性
iOS的category是不能扩展存储属性的,但是我们可以通过运行时关联来扩展“属性”。
假设扩展下面的“属性”:
// 由于扩展不能扩展属性,因此我们这里在实现文件中需要利用runtime实现。
typedef void(^CallBack)();
@property (nonatomic, copy) CallBack callback;
在实现文件中,我们用一个静态变量作为key:
static char CallbackKey;
- (void)setCallback:(CallBack)callback {
objc_setAssociatedObject(self, &CallbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (CallBack)callback {
return objc_getAssociatedObject(self, &CallbackKey);
}
其实就是通过objc_getAssociatedObject取得关联的值,通过objc_setAssociatedObject设置关联。
对于动态获取属性的名称、属性值使用较多的地方一般是在使用第三方库中,比如MJExtension。该第三方库是通过动态获取模型类的属性,并通过KVC的setValue:forKey来写入到模型的,实现两者间的转换(即将Model转换成字典,或者将字典转换成Model)。
待续。。。。。。
这里有一篇中文版挺不错的讲解Runtime博客。地址:http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/。