简要简介
Objective-C是基于C语言加入了面向对象特性和消息转发机制的动态语言,这意味着它不仅需要一个编译器,还需要Runtime系统来动态创建类和对象,进行消息发送和转发。
当执行[object doSomething]会被编译器转化为:objc_msgSend(receiver, selector)
如果消息含有参数,则为,objc_msgSend(receiver, selector, arg1, arg2, ...)
如果消息的接受者能够找到对应的selector
,那么就直接执行这个方法;否则,消息要么被转发,或者临时向接受者动态添加这个selector对应的实现内容;如果最后没有找到任何解决办法,就干脆崩溃掉。
最后,可以看出[object doSomething]真不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接受者发送doSomething
这条信息,而object如何响应这条信息,那就要看运行时发生的情况来决定了。
iOS RunTime之二:数据结构
objc_class、元类(Meta Class)、SEL、IMP、Method、Ivar
SEL:Objective-C在编译时,会依据每个方法的名字、参数序列,生成一个唯一的整型标识(int
类型的地址)这个标识就是SEL
。在本质上,SEL只是一个指向方法的指针(被hash化得KEY值),能提高方法的查询速度。
IMP:就是Implementation的缩写,本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。
Method:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
方法名 类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
method_imp指向了方法的实现,本质上是一个函数指针。
Ivar:是一种代表类中实例变量的类型。
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
}
Cache:是一个存储Method的链表,主要是为了优化方法调用的性能。
iOS RunTime之三:消息发送
objc_msgSend它具体是如何发送消息:
- 检查selector是否需要忽略。
- 检查target是否为nil。如果为nil,直接cleanup,然后return。这就是我们可以向nil发送消息的原因。
- 然后再target的Class中根据Selector去找IMP。
- 寻找IMP的过程:
1、先从当前class的cache方法列表(cache methodLists)中去找;
2、找到了,就调到对应函数实现;
3、没找到,没就从class的方法列表(methodLists)中找;
4、还没找到,就到super class中重复上述步骤找,直到找到基类(NSObject)为止
5、最后再找不到,就会进入动态方法解析和消息转发的机制。
理解:
Objective-C的动态绑定:消息的发送其实就是先确定object接受者对象,然后根据isa指针查找其方法然后跳转过去并执行。但是编译期间,是无法确定object接受者对象。只有在程序运行期间,object接受者对象才能得到确定。这种在运行期间才确定object接受者对象,Objective-C称为动态绑定。
C++或者Java调用对象的函数,函数与对象之间的关系,在编译期间就必须严格确定。
消息发送的机制使得在不重新编译的情况下,在运行期间,干预或者说hook原来的target(方法、变量等)变得更易于实现,更有实际应用价值,这个是需要依赖于消息发送和动态绑定的实现机制——Runtime。
iOS RunTime之四:消息转发
消息发送和消息转发流程可以概括为:消息发送是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;消息转发是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。
消息转发过程:
1、动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;
举例:
void dynamicMethodIMP(id self, SEL _cmd){
//实现...
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL{
if(aSEL == @selector(resolveThisMethodDynamically)){
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
2、重定向
- (id)forwardingTargetForSelector:(SEL)aSelector;
在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象:
- (id) forwardingTargetForSelector:(SEL)aSelector{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
3、转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
实现转发简单代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSString *sel = NSStringFromSelector(aSelector);
if ([sel isEqualToString:@"fly"]) {
//signatureWithObjcTypes 手动生成签名
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
Car *car = [[Car alloc] init];
if([car respondsToSelector:[anInvocation selector]]){
[anInvocation invokeWithTarget:car];
}
}
1、methodSignatureForSelector用来生成方法签名,这个签名就是给forwardInvocation中的参数NSInvocation调用的。
2、unrecognized selector sent to instance,原来就是因为methodSignatureForSelector这个方法中,由于没有找到fly对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。
iOS RunTime之五:Category不能动态添加成员变量
- 为什么Category中不能动态添加成员变量?
在runtime函数中,确实有一个class_addIvar()
函数用于给类添加成员变量,但是阅读过苹果的官方文档的人应该会看到:
This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.
大概的意思说,这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。
- 为什么不能为一个类动态的添加成员变量,可以给类动态增加方法和属性?
因为方法和属性并不“属于”类实例,而成员变量“属于”类实例。我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。
iOS RunTime之六:Category
代码介绍:
在runtime.h中查看定义:
typedef struct objc_category *Category;
是一个objc_category结构体,定义如下:
struct objc_category {
char *category_name OBJC2_UNAVAILABLE;
char *class_name OBJC2_UNAVAILABLE;
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list *class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
}
objc源代码,在objc-runtime-new.h中我们可以发现:
struct category_t {
const char *name; //是指 class_name 而不是 category_name
classref_t cls; //要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象
struct method_list_t *instanceMethods; //category中所有给类添加的实例方法的列表
struct method_list_t *classMethods;//category中所有给类添加的实例方法的列表
struct protocol_list_t *protocols;//category实现的所有协议的列表
struct property_list_t *instanceProperties;
//表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject
//和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。
};
从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。
Category和Extension的区别
1、Extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。Extension一般用来隐藏类的私有信息,你必须有一个类才能为这个类添加Extension,所以你无法为系统的类比如NSString添加Extension。
2、Category则完全不一样,它是在运行期决议的。
3、Extension可以添加属性、成员变量,而Category一般不可以。
总之,就Category和Extension的区别来看,Extension可以添加实例变量,而Category是无法添加实例变量的。因为Category在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。
面试题
一般面试官有时候会问到这样的问题:
在类和Category中都可以有study方法,那么有两个问题:
- 在类的study方法调用的时候,我们可以调用Category中声明的study方法么?
- 如果一个类有多个分类的时候study方法,调用顺序是咋样的呢?
答:study方法的执行顺序是先类,后Category,而Category的study方法执行顺序是根据编译顺序决定的。
Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。