C语言:调用函数的语言在声明完函数后,如果没有实现函数,程序是无法编译通过的。
OC:程序是可以编译通过的,但是会有一个黄色的警告。只有当程序运行之后才会出现如下的崩溃信息:
+[Function max::]: unrecognized selector sent to class 0x1000010f8***
Terminating app due to uncaught exception 'NSInvalidArgumentException'...
消息传递和调用函数对于程序员来说最大的区别就在于源代码编译的过程中是否能够编译通过.
解释消息传递机制的原理就要用到OC语言中的运行时系统(Runtime)了.
什么是Runtime
runtime是一个c和汇编写的动态库,它就像一个小小的系统,将OC和C紧密关联。这个系统主要做两件事 :
1、封装C语言的结构体和函数,让开发者在运行时创建、检查或者修改类、对象和方法等等。
2、传递消息,找出方法的最终执行代码。
那么我们可以利用这些方法干点什么?
1、遍历对象的属性
2、动态添加/修改属性,动态添加/修改/替换方法
3、动态创建类/对象/协议等等
4、方法拦截调用
运行时系统是一个提供一系列公开函数接口以及数据结构的动态链接库,这些头文件位于/usr/include/objc。许多这些函数允许你使用纯C语言重写当你写OC代码后编译器做的事情。其他形式的接口则是通过NSObject类中定义的一些方法。这些方法是可以用来实现其他的运行时接口来提高运行效率。但是重写运行时的代码对于使用OC语言进行编程并非是必须的,但是,少数的运行时函数在一些特殊情况下,对于OC程序还是很有用途的。
我们写的代码在程序运行过程中都会被转化成runtime的C代码执行,例如
[target doSomething];
会被转化成objc_msgSend(target, @selector(doSomething));
。
OC中一切都被设计成了对象,我们都知道一个类被初始化成一个实例,这个实例是一个对象。实际上一个类本质上也是一个对象,在runtime中用结构体表示。
如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector对应的实现内容,要么就干脆玩完崩溃掉。
现在可以看出[receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。
Objective-C 的 Runtime 铸就了它动态语言的特性。Objc Runtime使得C具有了面向对象能力,在程序运行时创建,检查,修改类、对象和它们的方法。可以使用runtime的一系列方法实现。
相关的定义:
/// 描述类中的一个方法
typedef struct objc_method *Method;
/// 实例变量
typedef struct objc_ivar *Ivar;
/// 类别Category
typedef struct objc_category *Category;
/// 类中声明的属性
typedef struct objc_property *objc_property_t;
类在runtime中的表示
//类在runtime中的表示
structobjc_class {
Class isa;//指针,顾名思义,表示是一个什么,实例的isa指向类对象,类对象的isa指向元类
#if!__OBJC2__
Class super_class;//指向父类
const char *name;//类名
long version;
long info;
long instance_size
struct objc_ivar_list *ivars//成员变量列表
struct objc_method_list **methodLists;//方法列表
struct objc_cache *cache;//缓存: 一种优化,调用过的方法存入缓存列表,下次调用先找缓存
struct objc_protocol_list *protocols//协议列表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
向object发送消息时,Runtime库会根据object的isa指针找到这个实例object所属于的类,然后在类的方法列表以及父类方法列表寻找对应的方法运行。id是一个objc_object结构类型的指针,这个类型的对象能够转换成任何一种对象。
然后再来看看消息发送的函数:objc_msgSend函数
在引言中已经对objc_msgSend进行了一点介绍,看起来像是objc_msgSend返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:
检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数了。
检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
如果 cache 找不到就找一下方法分发表。
如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
如果还找不到就要开始进入动态方法解析了,后面会提到。
方法调用
让我们看一下方法调用在运行时的过程
如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。
- 首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
- 如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
- 如果没找到,去父类指针所指向的对象中执行1,2.
- 以此类推,如果一直到根类还没找到,转向拦截调用。
- 如果没有重写拦截调用的方法,程序报错。
以上的过程给我带来的启发:
- 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
- 如果想调用已经重写过的方法的父类的实现,只需使用super这个编译器标识,它会在运行时跳过在当前的类对象中寻找方法的过程。
拦截调用
在方法调用中说到了,如果没有找到方法就会转向拦截调用。
那么什么是拦截调用呢。
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation*)anInvocation;
- 第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
- 第二个方法和第一个方法相似,只不过处理的是实例方法。
- 第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
- 第四个方法是将你调用的不存在的方法打包成
NSInvocation
传给你。做完你自己的处理后,调用invokeWithTarget:
方法让某个target触发这个方法。
动态添加方法
重写了拦截调用的方法并且返回了YES,我们要怎么处理呢?有一个办法是根据传进来的SEL
类型的selector动态添加一个方法。
首先从外部隐式调用一个不存在的方法:
//隐式调用方法
[target performSelector:@selector(resolveAdd:) withObject:@"test"];
然后,在target对象内部重写拦截调用的方法,动态添加方法。
void runAddMethod(id self, SEL _cmd, NSString *string){
NSLog(@"add C IMP ", string);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
//给本类动态添加一个方法
if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
}
return YES;
}
其中class_addMethod
的四个参数分别是:
-
Class cls
给哪个类添加方法,本例中是self -
SEL name
添加的方法,本例中是重写的拦截调用传进来的selector。 -
IMP imp
方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;
获得方法的实现。 -
"v@:*"
方法的签名,代表有一个参数的方法。
关联对象
现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。
这种情况的一般解决办法就是继承。
但是,只增加一个属性,就去继承一个类,总是觉得太麻烦类。
这个时候,runtime的关联属性就发挥它的作用了。
你还可以把添加和获取关联对象的方法写在你需要用到这个功能的类的类别中,方便使用。
方法交换
方法交换,顾名思义,就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。
方法交换应该被保证,在程序中只会执行一次
应用实例
1、Json到Model的转化
在开发中相信最常用的就是接口数据需要转化成Model了(当然如果你是直接从Dict取值的话。。。),很多开发者也都使用著名的第三方库如JsonModel、Mantle或MJExtension等,下面我们使用runtime去解析json来给Model赋值。
原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。
核心方法:在NSObject的分类中添加方法:
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
//(1)获取类的属性及属性对应的类型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute]; }
//立即释放properties指向的内存
free(properties);
//(2)根据类型给属性赋值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
2、快速归档
有时候我们要对一些信息进行归档,如用户信息类UserInfo,这将需要重写initWithCoder和encodeWithCoder方法,并对每个属性进行encode和decode操作。那么问题来了:当属性只有几个的时候可以轻松写完,如果有几十个属性呢?
原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。
核心方法:在Model的基类中重写方法:
- (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];
}
}
3、访问私有变量
我们知道,OC中没有真正意义上的私有变量和方法,要让成员变量私有,要放在m文件中声明,不对外暴露。如果我们知道这个成员变量的名称,可以通过runtime获取成员变量,再通过getIvar来获取它的值。
方法:
Ivar ivar = class_getInstanceVariable([Model class], "_str1");
NSString * str1 = object_getIvar(model, ivar);