一、Runtime基石:Objective-C对象模型
1、对象
每一个对象都是类的实例, 类中保存对象的方法列表;当一个对象方法被调用时,类会首先查找它本身是否有该方法的实现,如果没有,则会向它的父类查找该方法,直到NSObject(根类);
类是元类 (metaclass) 的实例;元类保存类方法列表;当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则会向它的父类查找该方法,直到NSObject(根类);
2、isa指针
对象的isa指针指向所属的类,类的isa指针指向所属的元类;所有的元类的isa指针都会指向一个根元类 (root metaclass)。根元类的isa指针指向自己,行成了一个闭环。
在64 位 CPU 下,isa 的内部结构有变化。具体查看用 isa 承载对象的类信息
对象、isa指针、类、元类、根元类的关系如下图:
3、对象布局
实例变量(包括父类)都保存在对象本身的存储空间内;实例方法保存在类中,类方法保存在元类中;父类的实例方法保存在各级 super class 中,父类的类方法保存在各级 super meta class;
//对象组成 --start--
isa pointer
rootClass's vars
penultimate superClass's vars
...
superClass's vars
Class's vars
//对象组成 --end--
typedef struct objc_class *Class;
//类的结构
struct objc_class{
struct objc_class* isa; //指向元类
struct objc_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; //协议列表
};
//实例变量的结构
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
}
说明1:对象中保存指向类的isa指针 以及 各级的 实例变量(ivar),这个内存结构在编译时就确定下来了,不能在编译时给对象增加实例变量。
说明2:类的内存布局有isa指针、super_class指针、实例变量列表、方法列表和协议列表,其中实例变量(var)包含了变量的名称、类型、偏移等。
二、Runtime核心:消息发送和转发
Runtime赋予了OC了诸多动态特性,使其可以在运行时可以做一些事情;主要表现为:动态类型(在运行时才检查对象类型)和动态绑定(接到消息后,由运行环境决定执行哪部分代码)
1、消息发送(Message)
Objective-C 中的方法调用,实质上是在底层用objc_msgSend()实现消息发送,其核心在于:根据SEL(选择器)开始找到IMP;其中SEL是实例方法的指针,可以看做方法名字符串;IMP是函数指针,指向方法实现的地址。
//调用方法
[obj doSomething];
//在编译时候转换
objc_msgSend(obj,@selector(doSomething))
objc_msgSend的定义如下:
// self是接收者,接收该消息的类的实例
// _cmd是选择器,要处理的消息的selector
// ... 是需传入的参数,参数个数不定
objc_msgSend(id self, SEL _cmd, ...)
objc_msgSend的发送流程:先在Class中的缓存查找imp(没缓存则初始化缓存),如果没找到,则向父类的Class查找。如果一直查找到根类仍旧没有实现,就走消息转发(_objc_msgForward)了。
给nil发送消息不会有什么作用,但是返回值有些区别,具体如下:
a) 如果方法返回值是 对象,返回nil
b) 如果方法返回值是 指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量
c) 如果方法返回值是 结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。
d) 如果方法返回值不是 上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。
2-1、消息转发(Message Forwarding)
消息转发解决的是:查找IMP(方法实现)失败后的处理;经历动态方法解析、备用接收者和完整的消息转发三个过程,其流程如下图:
动态方法解析:接收到未知消息时,Runtime向当前类发送+resolveInstanceMethod:或+resolveClassMethod:消息,在这里可以添加缺失的方法,返回YES,重新发送消息,否则继续下一步;
备用接收者:动态方法解析中没能处理,Runtime会向forwardingTargetForSelector:发消息,如果该方法返回了一个非nil或非self对象,恰好该对象实现了这个方法,那么该对象就成了消息的接收者,消息就被分发到该对象。
完整消息转发:前两个都没能处理好,Runtime发送methodSignatureForSelector:消息,获取selector对应方法的签名;如果有方法签名返回,则根据方法签名创建描述消息的NSInvocation,向当前对象发送forwardInvocation:消息;如果没有方法签名返回,返回nil,向当前对象发送doesNotRecognizeSelector:消息,应用Crash退出。
2-2、避免消息转发的办法
在消息转发三个过程中,未知消息的处理过程越往后,代价越大;一般我们可以这么做 尽可能避免消息转发,可以这么做:
调用delegate 方法前检查方法是否实现(respondsToSelector:), 只有实现了(respondsToSelector:返回YES) ,才去真正调用delegate 方法。
if([self.delegate respondsToSelector: @selector(sayHello)]) {
[self.delegate sayHello];
}
直接调用方法,少用performSelector:;因为在直接调用方法时,编译自动校验,如果方法不存在,编译器会直接报错;而使用performSelector:的话一定是在运行时候才能发现,如果此方法不存在就会崩溃。
//直接使用方法调用,少使用performSelector
[dog sayHello];
// [dog performSelector:@selector(sayHello) withObject:nil];
使用performSelector:,最好先判断方法是否实现(respondsToSelector:),只有实现了(respondsToSelector:返回YES) ,才去调用performSelector:方法。
//respondsToSelector:和performSelector:组合使用
if ([dog respondsToSelector:@selector(sayHello)]) {
[dog performSelector:@selector(sayHello)];
}
强制类型转换,先判断对象是否属于强制转换后的类
if([data isKindOfClass:[NSDictionary class]]){
//
}
三、Runtime特性和应用
1、分类(Category)
原理:对象的方法定义都保存在类的可变区域中,修改methodLists指针指向的指针的值,就可以实现动态地为某一个类增加成员方法。(但是对象布局在编译时候就固定了,结构体的大小并不能动态变化,在运行时不能增加实例变量)。
通过关联objc_setAssociatedObject 和 objc_getAssociatedObject方法可以变相地给对象增加实例变量,并不会真正改变了对象的内存结构。
通过Category新增的方法,会插入到方法列表的前部;如果有和原来方法重名,在运行时,顺序查找时,一旦找到对应名字的方法,就不再查找,导致原来方法得不到机会,这是Category新增的方法和原方法重名,原有方法失效的原因。
作用:给现有的类添加方法;将一个类的实现拆分成多个独立的源文件;声明私有的方法。
2、关联对象(Associated Objects)
原理:Category不能给一个已有类添加实例变量,但是可以通过关联对象添加属性;但是关联对象不会改变对象的内存布局,新增的属性是添加到和对象地址关联的哈希表中;
Associated Objects 相关的三个方法
objc_setAssociatedObject //添加关联对象
objc_getAssociatedObject //获取关联对象
objc_removeAssociatedObjects // 删除所有关联对象
作用:为现有的类添加私有变量以帮助实现细节;为现有的类添加公有属性;为 KVO 创建一个关联的观察者
3、方法混写(Method Swizzling)
原理:在运行时交换方法实现(IMP)
作用:可以利用它hook原有的方法,插入自己的业务需求,
4、键值观察(KVO)
观察者模式在Objective-C的应用之一,借助Runtime特性,实现自动键值观察;使用了isa swizzling机制。具体描述如下:
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个子类,在这个子类中重写基类中被观察属性的 setter 方法,实现真正的通知机制;
派生类还重写了 class 方法以“欺骗”外部调用者,系统将对象的 isa 指针指向这个新诞生的子类,实质上这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。
此外,派生类还重写了 dealloc 方法来释放资源。
说明:KVC(键值编码)是不通过存取方法,而通过属性名称字符串间接访问属性的机制,没有用到isa swizzling机制。
5、NSProxy
OC是单继承的,但是可以利用NSProxy实现一下“伪多继承”,具体参考NSProxy——少见却神奇的类
项目中,主要是利用NSProxy做消息转发的代理类,如弱引用代理类,可以打破循环引用。
@interface FLWeakProxy : NSProxy
+ (instancetype)weakProxyForObject:(id)targetObject;
@end
@interface FLWeakProxy ()
@property (nonatomic, weak) id target;
@end
@implementation FLWeakProxy
#pragma mark Life Cycle
//类没有定义默认的init方法.
+ (instancetype)weakProxyForObject:(id)targetObject{
FLWeakProxy *weakProxy = [FLWeakProxy alloc];
weakProxy.target = targetObject;
return weakProxy;
}
#pragma mark Forwarding Messages
- (id)forwardingTargetForSelector:(SEL)selector{
// Keep it lightweight: access the ivar directly
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation{
void *nullPointer = NULL;
[invocation setReturnValue:&nullPointer];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector{
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
@end
说明: NSProxy非常适合做消息转发的代理类,能自动转发中定义的接口和NSObject的Category中定义的方法,如果使用NSObject来做,不能自动转发NSObject的Category中定义、respondsToSelector:、isKindOfClass:这两个方法。