Objective-C是一门动态语言,可以在运行的时候动态决定调用哪个方法实现,甚至增加、替换方法的具体实现,而这些都归功于Objective-C的运行时(runtime)系统。
一. Runtime简介
Runtime 又叫运行时,是一套底层的 C 语言 API,是 iOS 系统的核心之一。开发者在编码过程中,可以给任意一个对象发送消息,在编译阶段只是确定了要向接收者发送这条消息,而接受者将要如何响应和处理这条消息,那就要看运行时来决定了。
C语言中,在编译期,函数的调用就会决定调用哪个函数。
而OC的函数,属于动态调用过程,在编译期并不能决定真正调用哪个函数,只有在真正运行时才会根据函数的名称找到对应的函数来调用。
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。
先说一下isa指针
要认识什么是isa指针,我们得先明确一点:
在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。
那么什么是类呢?在xcode中用快捷键Shift+Cmd+O 打开文件objc.h 能看到类的定义:
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
可以看出:
Class 是一个 objc_class 结构类型的指针, id是一个 objc_object 结构类型的指针.
我们再来看看 objc_class 的定义:
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:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
super_class:父类,如果该类已经是最顶层的根类,那么它为NULL。
version:类的版本信息,默认为0
info:供运行期使用的一些位标识。
instance_size:该类的实例变量大小
ivars:成员变量的数组
再来看看各个类实例变量的继承关系:
每一个对象本质上都是一个类的实例。其中类定义了成员变量和成员方法的列表。对象通过对象的isa指针指向类。
每一个类本质上都是一个对象,类其实是元类(meteClass)的实例。元类定义了类方法的列表。类通过类的isa指针指向元类。
所有的元类最终继承一个根元类,根元类isa指针指向本身,形成一个封闭的内循环。
OC的动态特性包含了以下三点:
1.动态类型
即运行时在决定对象的类型。 -isMemberOfClass: isKindOfClass: 这两个方法 用于判断类或实例变量是属于哪个类。
2.动态绑定
基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。在继续之前,需要明确 Objective-C中消息的概念。由于OC的动态特性,在OC中其实很少提及“函数”的概念,传统的函数一般在编译时就已经把参数信息和函数实现打包 到编译后的源码中了,而在OC中最常使用的是消息机制。调用一个实例的方法,所做的是向该实例的指针发送消息,实例在收到消息后,从自身的实现中寻找响应 这条消息的方法。动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。这里所指的属性和方法当然包括了原来没有在类中实现的,而是在运行 时才需要的新加入的实现。
3、动态加载
根据需求加载所需要的资源,这点很容易理解,对于iOS开发来说,基本就是根据不同的机型做适配。最经典的例子就是在Retina设备上加载@2x 的图片,而在老一些的普通屏设备上加载原图。随着Retina iPad的推出,和之后可能的Retina Mac的出现,这个特性相信会被越来越多地使用。
runtime - 方法调用的实质
当我们写下一行代码
[obj doSth];
,在编译时,编译器会将我们的代码转化为
objc_msgSend(obj,@selector(doSth));
objc_msgSend()方法实现了函数查找和匹配,下面是它的原理: 根据对象obj找到对象类中存储的函数列表methodLists。 再根据SEL@selector(doSth)在methodLists中查找对应的函数指针method_imp。 根据函数指针method_imp调用响应的函数。
发送消息的步骤 (方法执行)
1.检测selector是否要执行,比如有了垃圾回收机制就不需要retain和release等方法选择器 。
2.检测targer是否为nil,Objective-C允许我们队一个nil对象执行方法,然后忽略掉。
3.上面都执行成功了,就开始查找这个方式实现的IMP,先从Cache中查找,如果找到了就执行。(Cache为方法调用的性能进行优化,也就是在方法调用的时候会首先从这个缓存列表中找,如果找到就执行,没有找到就通过isa指针在类的方法列表中寻找,找到之后执行并把这个方法添加到这个缓存列表中,以供下一次调用,当下一次调用的时候直接从缓存列表中找,这样就提高了的性能,不用通过isa指针去查找。
4.如果找不到就在类的方法列表中去查找
5.如果在类的方法列表中找不到就去父类的方法列表中去查找,一直到NSObject为止。
6.如果还找不到,当runtime系统在Cache和方法分发列表中找不到要执行的方法的时候,runtime会调用
+(BOOL)resolveInstanceMethod:(SEL)sel (实例方法)
+(BOOL)resolveClassMethod:(SEL)sel (类方法)
这两个方法给我们一次动态添加方法的机会。我们可以用
class_addMethod
向特定类或类实例添加方法的功能。
例子:
void eat (id self,SEL sel){
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(eat)) {
// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址)
// 第四个参数:函数的类型,(返回值+参数类型)
// v:void
// @:对象->self
// : 表示SEL->_cmd
class_addMethod(self,sel, eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
详细解释 class_addMethod 的最后一个参数
例子
-(int)say:(NSString *)str;
相应的实现函数就应该是这样:
int say(id self, SEL sel, NSString *str)
{
NSLog(@"%@", str);
return 100;//随便返回个值
}
class_addMethod这句就应该这么写:
class_addMethod([EmptyClass class], @selector(say:), (IMP)say, "i@:@");
其中types参数为"i@:@“,
按顺序分别表示:
i 表示 方法 say 中的返回值
@ 表示 方法 say 中的第一个参数 self
: 表示 方法 say 中的第二个参数 sel
@ 表示 方法 say 中的第三个参数 str
这些表示方法都是定义好的(Type Encodings),关于Type Encodings的其他类型定义请参考官方文档
7.如果上面的resolveInstanceMethod:或者resolveClassMethod:返回NO,消息转发就会进入消息转发机制,但是runtime又给我们一次机会再次修改接受者的机会,即当前的接受者不能收到这个消息,我们通过重载
- (id)forwardingTargetForSelector:(SEL)aSelector
这个消息转发给其他能接受消息的对象。
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(weight)) {
People *people = [People new];
return people;
}
return [super forwardingTargetForSelector:aSelector];
}
runtime - 给类别添加属性 - 动态添加属性
#import "LQPerson.h"
@interface LQPerson (nameProperty)
@property (copy, nonatomic) NSString *name;
@end
#import "LQPerson+nameProperty.h"
#import <objc/runtime.h>
// 定义关联的key
static const char *key = "name";
@implementation LQPerson (nameProperty)
-(NSString *)name{
return objc_getAssociatedObject(self, key);
}
-(void)setName:(NSString *)name{
/*
OBJC_ASSOCIATION_ASSIGN; //assign策略
OBJC_ASSOCIATION_COPY_NONATOMIC; //copy策略
OBJC_ASSOCIATION_RETAIN_NONATOMIC; // retain策略
OBJC_ASSOCIATION_RETAIN;
OBJC_ASSOCIATION_COPY;
*/
/*
* id object 给哪个对象的属性赋值
const void *key 属性对应的key
id value 设置属性值为value
objc_AssociationPolicy policy 使用的策略,是一个枚举值,和copy,retain,assign是一样的,手机开发一般都选择NONATOMIC
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
*/
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
主要用到 runtime 中的
objc_getAssociatedObject
objc_setAssociatedObject
这个两个方法
runtime - 方法交换 - 给系统中的方法添加额外的实现
#import "UIImage+LogImageName.h"
#import <objc/runtime.h>
@implementation UIImage (LogImageName)
+(void)load{
Method imageName = class_getClassMethod(self, @selector(imageNamed:));
Method imageName_log = class_getClassMethod(self, @selector(imageName_log:));
method_exchangeImplementations(imageName_log, imageName);
}
+(instancetype)imageName_log:(NSString*)name{
//这里调用 imageName_log 相当于 imageNamed
UIImage * image = [self imageName_log:name];
if (image == nil) {
NSLog(@"***************runTime没有该图片***************");
}
return image;
}
@end
这里要说一下
+ (void)load//只要在工程中创建了 这个类 就会调用
+ (void)initialize//创建时不会调用 只有在第一次使用时才会被调用
**这俩个方法 都只会别调用一次**
runtime - 动态的遍历一个类的所有成员变量,用于字典转模型,归档解档操作
Ivar *ivars = class_copyIvarList([BDPerson class], &count);//获得一个指向该类成员变量的指针
for (int i =0; i < count; i ++) {
//获得Ivar
Ivar ivar = ivars[i]; //根据ivar获得其成员变量的名称--->C语言的字符串
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
NSLog(@"%d----%@",i,key);
}
动态创建一个类
KVO的底层实现
参考博客:
http://ios.jobbole.com/89209/
http://www.jianshu.com/p/41735c66dccb