转自南华coder的空间
Objective-C利用Runtime库(底层的 C 语言 API)赋予了语言的动态特性。
每一个对象都是 类的实例 , 类中保存 对象的方法列表 ;当一个对象方法被调用时,类会首先查找它本身是否有该方法的实现,如果没有,则会向它的父类查找该方法,直到NSObject(根类);
类是 元类 (metaclass) 的实例 ;元类保存 类方法列表 ;当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则会向它的父类查找该方法,直到NSObject(根类);
对象的 isa指针 指向所属的类,类的 isa指针 指向所属的元类;所有的元类的 isa指针 都会指向一个根元类 (root metaclass)。根元类的 isa指针 指向自己,行成了一个闭环。
在64 位 CPU 下,isa 的内部结构有变化。具体查看用 isa 承载对象的类信息
对象、isa指针、类、元类、根元类的关系如下图:
实例变量(包括父类)都保存在对象本身的存储空间内;实例方法保存在 类 中,类方法保存在 元类 中;父类的实例方法保存在各级 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赋予了OC了诸多动态特性,使其可以在运行时可以做一些事情;主要表现为: 动态类型 (在运行时才检查对象类型)和 动态绑定 (接到消息后,由运行环境决定执行哪部分代码)
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 的消息的返回值将是未定义的。
消息转发解决的是:查找IMP(方法实现)失败后的处理;经历 动态方法解析 、 备用接收者 和 完整的消息转发 三个过程,其流程如下图:
动态方法解析:接收到未知消息时,Runtime向当前类发送+resolveInstanceMethod:或+resolveClassMethod:消息,在这里可以 添加缺失的方法 ,返回YES,重新发送消息,否则继续下一步;
备用接收者: 动态方法解析 中没能处理,Runtime会向forwardingTargetForSelector:发消息,如果该方法返回了一个非nil或非self对象,恰好该对象实现了这个方法,那么该对象就成了消息的接收者,消息就被分发到该对象。
完整消息转发:前两个都没能处理好,Runtime发送methodSignatureForSelector:消息,获取selector对应方法的签名;如果有方法签名返回,则根据方法签名创建描述消息的NSInvocation,向当前对象发送forwardInvocation:消息;如果没有方法签名返回,返回nil,向当前对象发送doesNotRecognizeSelector:消息,应用Crash退出。
在消息转发三个过程中,未知消息的处理过程越往后,代价越大;一般我们可以这么做 尽可能避免消息转发,可以这么做:
调用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]]){ // }
原理:对象的 方法 定义都保存在类的可变区域中,修改methodLists指针指向的指针的值,就可以实现动态地为某一个类增加 成员方法 。( 但是对象布局在编译时候就固定了,结构体的大小并不能动态变化,在运行时不能增加实例变量 )。
通过关联objc_setAssociatedObject 和 objc_getAssociatedObject方法可以变相地给对象增加实例变量,并不会真正改变了对象的内存结构。
通过Category新增的方法,会插入到方法列表的前部;如果有和原来方法重名,在运行时,顺序查找时,一旦找到对应名字的方法,就不再查找,导致 原来方法 得不到机会, 这是Category新增的方法和原方法重名,原有方法失效的原因 。
作用:给现有的类添加方法;将一个类的实现拆分成多个独立的源文件;声明私有的方法。
原理:Category不能给一个已有类添加实例变量,但是可以通过 关联对象 添加属性;但是关联对象不会改变对象的内存布局,新增的属性是添加到和对象地址关联的哈希表中;
Associated Objects 相关的三个方法
objc_setAssociatedObject //添加关联对象 objc_getAssociatedObject //获取关联对象 objc_removeAssociatedObjects // 删除所有关联对象
作用:为现有的类添加私有变量以帮助实现细节;为现有的类添加公有属性;为 KVO 创建一个关联的观察者
具体参考: Objective-C Associated Objects 的实现原理
原理:在运行时交换方法实现(IMP)
作用:可以利用它hook原有的方法,插入自己的业务需求,
详细参考我写的 Method Swizzling小记
观察者模式在Objective-C的应用之一,借助Runtime特性,实现自动键值观察;使用了isa swizzling机制。具体描述如下:
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个 子类 ,在这个子类中 重写 基类中被观察属性的 setter 方法,实现真正的通知机制;
派生类还重写了 class 方法以“欺骗”外部调用者,系统将对象的 isa 指针指向这个 新诞生的子类 ,实质上这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。
此外,派生类还重写了 dealloc 方法来释放资源。
说明:KVC(键值编码)是不通过存取方法,而通过属性名称字符串间接访问属性的机制,没有用到isa swizzling机制。