第一篇:摘抄自 Objective-C Runtime的那点事儿一消息机制
RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。编译完成之后直接顺序执行,无任何二义性。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
那OC是怎么实现动态调用的呢?下面我们来看看OC通过发送消息来达到动态调用的秘密。假如在OC中写了这样的一个代码:
[obj makeText];
其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成
objc_msgSend(obj,@selector(makeText));
obj解释
首先看obj这个对象,iOS中的obj都继承于NSObject。我们看到在NSObject中存在一个Class的isa指针:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
isa的类型为结构体
//An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
-->
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //指向metaclass
#if !__OBJC2__
Class super_class; //指向其父类
const char *name; //类名
long version; //类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion 和 class_getVersion 进行修改、读取
long info; //一些标识信息,如CLS_CLASS(0x1L)表示该类为普通class,其中包含对象方法和成员变量;CLS_META(0x2L)表示该类为 metaclass,其中包含类方法
long instance_size; // 该类的实例变量大小(包括从父类继承下来的实例变量)
struct objc_ivar_list *ivars; //用于存储每个成员变量的地址
struct objc_method_list **methodLists; //与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法
struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率;
struct objc_protocol_list *protocols; // 存储该类遵守的协议
#endif
} OBJC2_UNAVAILABLE;
解释:
Class isa
: 指向metaclass,也就是静态的Class。
一般<u>Object对象中的isa指向普通的Class</u>,这个Class中存储普通成员变量和对象方法(- 开头的方法),<u>普通Class中的isa指针指向静态Class</u>,静态Class中存储static类型成员变量和类方法(+ 开头的方法)。
Class super_class
: 指向父类,如果这个类是根类,则为NULL。
注意:
所有metaclass中isa指针都指向根metaclass,而根metaclass则指向自身。
if !__OBJC2__ endif
标记的属性是Ojective-C 2.0不支持的,但实际上可以用响应的函数获取这些属性,例如:如果想要获取Class的name属性,可以按如下方法获取:
Class classPerson = Person.class;
// printf("%s\n", classPerson->name); //用这种方法已经不能获取name了 因为OBJC2_UNAVAILABLE
const char *cname = class_getName(classPerson);
printf("%s", cname); // 输出:Person
@selector(makeText) :
这是一个SEL方法选择器。SEL主要作用是快速地通过方法名字(makeText)查找对应方法的函数指针,然后调用其函数。
SEL本身是一个Int类型的一个地址,地址中存放着方法的名字。在一个类中,每个方法对应着一个SEL,所以不能存在多个名称相同的方法,即使参数类型不同。因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。
那么消息发送之后是怎么来动态查找对应的方法的?
-->首先编译器会将 [obj makeText];
转化为 objc_msgSend(obj, @selector(makeText));
。
然后在 objc_msgSend函数中,首先通过obj的isa指针找到obj对应的class。在Class中先去cache
中通过SEL查找对应函数method(猜测cache中的method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度)
,若cache中没有找到,则到methodLists
中查找,若找不到,则取superClass中查找。若找到了,则将method加入到cache中
,以便下次查找,并通过method中的函数指针跳转到对应的函数中去执行
。
第二篇:iOS~runtime理解
我们写的代码在程序运行过程中都会被转化成runtime的C代码执行,例如[target doSomething];
会被转化成objc_msgSend(target, @selector(doSomething));
。
相关的定义
/// An opaque type that represents a method in a class definition.
//类中的一个方法
typedef struct objc_method *Method;
/// An opaque type that represents an instance variable.
// 实例变量
typedef struct objc_ivar *Ivar;
/// An opaque type that represents a category.
// 类别 Category
typedef struct objc_category *Category;
/// An opaque type that represents an Objective-C declared property.
// 类中声明的属性
typedef struct objc_property *objc_property_t;
获取各种列表的方法:
//获取列表
- (void)getList{
unsigned int count;
//获取属性列表
objc_property_t *propertyList = class_copyPropertyList([Person class], &count);
for (unsigned int i=0; i < count; i++) {
objc_property_t property = propertyList[i];
const char *propertyName = property_getName(property);
NSLog(@"property ----> %@",[NSString stringWithUTF8String:propertyName]);
}
//获取方法列表
Method *methodList = class_copyMethodList([Person class], &count);
for (unsigned int i=0; i < count; i++) {
Method method = methodList[i];
NSLog(@"method----> %@",NSStringFromSelector(method_getName(method)));
}
//获取成员变量列表
Ivar *ivars = class_copyIvarList([Person class], &count);
for (unsigned int i=0; i < count; i++) {
Ivar ivar = ivars[i];
const char * ivarName = ivar_getName(ivar);
NSLog(@"Ivar ----> %@",[NSString stringWithUTF8String:ivarName]);
}
//获取协议列表
__unsafe_unretained Protocol ** protocolList = class_copyProtocolList([Person class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
}
方法调用过程
如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。
--> 1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
--> 2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现并执行。
--> 3.如果没找到,去父类指针所指向的对象中执行1,2。
--> 4.依次类推,如果一直到根类还没找到,转向拦截调用。
--> 5.如果没有重写拦截调用的方法,程序报错。
以上的过程给我带来的启发:
- 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
- 如果想调用已经重写过的方法的父类的实现,只需使用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 -> 我们是怎么实现的呢?
<u>有一个办法是根据传进来的SEL
类型的selector动态添加一个方法:</u>
//隐式调用方法
[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@:*");
}
}
其中 class_addMethod
的四个参数分别是:
1、Class cls
给哪个类添加方法,本例中是self
2、SEL name
添加的方法,本例中是重写的拦截调用传进来的selector。
3、IMP imp
方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用 +(IMP)instanceMethodForSelector:(SEL)aSelector;
获得方法的实现。
4、"v@:*"
方法的签名,代表有一个参数的方法。
关联对象
现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。
这种情况的一般解决办法就是继承。
但是,只增加一个属性,就去继承一个类,总是觉得太麻烦类。
这个时候,runtime的关联属性就发挥它的作用了。
//首先定义一个全局变量,用它的地址作为关联对象的key
static char associatedObjectKey;
//设置关联对象
objc_setAssociatedObject(target, $associatedObjectKey, @"添加的字符串属性", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//获取关联对象
NSString *string = objc_getAssociatedObject(target, & associatedObjectKey);
NSLog(@"AssociatedObject = %@", string);
objc_setAssociatedObject
的四个参数:
-
id object
给谁设置关联对象。
2.const void *key
关联对象唯一的key,获取时会用到。
3.id value
关联对象。
4.objc_AssociationPolicy
关联策略,有以下几种策略:
如果你熟悉OC,看名字应该知道这几种策略的意思了吧。
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
objc_getAssociatedObject
的两个参数
1.id object
获取谁的关联对象。
2.const void *key
根据这个唯一的key获取关联对象。
其实,你还可以把添加和获取关联对象的方法写在你需要用到这个功能的类的类别中,方便使用。
//添加关联对象
- (void)addAssociatedObject:(id)object{
objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//关联策略依情况而定
}
//获取关联对象
- (id)getAssociatedObject{
return objc_getAssociatedObject(self, _cmd);
}
注意:这里面我们把getAssociatedObject方法的地址作为唯一的key,
_cmd代表当前调用方法的地址。
方法交换 (详见原文)
方法交换,顾名思义,就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。
话不多说,这是参考Mattt大神在NSHipster上的文章自己写的代码。
方法交换对于我来说更像是实现一种思想的最佳技术:AOP面向切面编程。
既然是切面,就一定不要忘记,交换完再调回自己。
一定要保证只交换一次,否则就会很乱。
最后,据说这个技术很危险,谨慎使用。