Objective-C是支持比较丰富的动态语言特性,能运行时修改类型,并支持类型的自省,相对于C++之类的静态语言,运行时自省是非常灵活自由的特性。
动态特性基础
包含头文件 #import <objc/runtime.h>
id类型
通用指针类型,弱类型,编译时不进行类型检查
– 如果不涉及到多态,尽量使用静态类型 – 静态类型可更好的在编译阶段而不是运行阶段指 出错误 – 静态类型能够提高程序的可读性
NSObject的动态性方法
NSObject还有些方法能在运行时获得类的信息,并检查一些特性:
+ (Class)class;
返回对象的类
@property(readonly) Class superclass;
返回父类的Class
- (BOOL)isKindOfClass:(Class)aClass;
检查对象是否是指定的类或其派生类的实例
- (BOOL)isMemberOfClass:(Class)aClass;
检查对象是否是指定的类的实例
- (BOOL)respondsToSelector:(SEL)aSelector;
检查对象能否响应指定的消息
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
检查对象是否实现了指定协议类的方法
- (IMP)methodForSelector:(SEL)aSelector;
则返回指定方法实现的地址
例子:
@interface MyClass : NSObject
- (void)doSomething:(NSString*)thing;
@end
@implementation MyClass
- (void)doSomething:(NSString*)thing {
NSLog(@"doSomething: %@", thing);
}
+ (BOOL)resolveInstanceMethod:(SEL)aSelector {
IMP myIMP = imp_implementationWithBlock(^(id _self, NSString *string) {
NSLog(@"Hello %@", string);
});
if (aSelector == @selector(myDynamicMethod:)) {
class_addMethod(self, aSelector, (IMP)myIMP, "v@:@");
return YES;
}
return [super resolveInstanceMethod:aSelector];
}
@end
id cls = [[MyClass alloc] init];
if ( [cls isKindOfClass:[MyClass class]] ) {
MyClass * mycls = (MyClass*)cls;
[mycls doSomething: @"home work"];
}
SEL method = @selector(doSomething:);
IMP imp = [cls methodForSelector:method];
if ( imp ) {
imp(cls, method, @"wash");
}
if ( [cls respondsToSelector:@selector(doSomething:)] ) {
NSLog(@"SEL doSomething supported");
}
if ( [cls respondsToSelector:@selector(myDynamicMethod:)] ) {
// output: Hello suninf
[cls performSelector:@selector(myDynamicMethod:) withObject:@"suninf"];
}
注意: IMP类型的对象在最新的xcode下不支持直接调用,需要xcode的选项设置(Enable Strict Check for objc_msgSend Call设置为NO)或显式的类型转换。
typedef void(*FUNC_TYPE)(id, SEL, NSString*);
SEL method = @selector(doSomething:);
IMP imp = [cls methodForSelector:method];
if ( imp ) {
FUNC_TYPE func = (FUNC_TYPE)imp;
func(cls, method, @"wash");
}
Class类型
Class NSClassFromString(NSString *aClassName);
由字符串得到类对象NSString *NSStringFromClass(Class aClass);
由类名得到字符串
NSString * sCls = NSStringFromClass([MyClass class]);
Class mCls = NSClassFromString(sCls);
id vCls = [[mCls alloc] init];
if ( [vCls isKindOfClass:mCls] ) {
[vCls doSomething:@"test"];
}
SEL类型
Objective-C在编译的时候,会根据方法的名字 (包括参数序列),生成一个用来区分这个方法的唯一的一个标示(ID),这个标示(ID)就是SEL类型,在运行时候是通过方法的标示来查找方法的。
- 可以通过@selector()指示符获得方法的标示。
NSSelectorFromString(NSString*);
根据方法名得到方法标识(NSString*)NSStringFromSelector(SEL);
得到SEL类型的方法名
IMP
IMP是”implementation”的缩写,它是objetive-C 方法 (method)实现代码块的地址,类似函数指针,通过它可以直接访问任意一个方法,免去发送消息的代价。
Objetive-C中的Method结构:
struct objc_method{
SEL method_name; //方法名
char *method_types; //方法地址
IMP method_imp; //方法地址(IMP)
};
typedef struct objc_method* Method;
class的方法列表其实是一个字典,key为selectors,IMPs为value。一个IMP是指向方法在内存中的实现。很重要的一点是,selector和IMP之间的关系是在运行时才决定的,而不是编译时。
动态特性深入
这类运行时特性大多由/usr/lib/libobjc.A.dylib
这个动态库提供,里面包括了对于类、实例成员、成员方法和消息发送的很多API,包括获取类实例变量列表,替换类中的方法,为类成员添加变量,动态改变方法实现等,十分强大。完整的API列表和手册可以在这里找到。
消息
在objective-c里,对象不调用方法,而是接收消息,消息表达式为: [receiver message];
运行时系统首先确定接收者的类型(动态类型识别),然 后根据消息名在类的方法列表里选择相依的方法执行,所以在源代码里消息也称为选择器(selector)。
- 首先通过第一个参数的receiver,找到它的isa指针,然后在isa指向的Class对象中使用第二个参数selector查找方法;
- 如果没有找到,就使用当前Class 对象中的新的isa 指针,到上一级的父类的Class 对象中查找;
- 当找到方法后,再依据receiver的中的self指针找到当前的对象,调用当前对象的具体实现的方法(IMP),然后传递参数,调用实现方法。
- 假如一直找到NSObject的Class 对象,也没有找到你调用的方法,就会报告不能识别发送消息的错误。
消息是通过objc_msgSend()
这个runtime方法及相近的方法来实现的。这个方法需要一个target,selector,还有一些参数。理论上来说,编译器只是把消息分发变成objc_msgSend来执行。
void objc_msgSend(id self, SEL cmd, ...);
比如下面这两行代码是等价的:
[array insertObject:foo atIndex:5];
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
Object, Class
Objective-C中的Object是一个结构体(struct),第一个成员是isa,指向自己的class。这是在objc/objc.h
中定义的。
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;
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
创建、修改、自省classes和objects
class
class开头的方法是用来修改和自省classes
- class_addIvar, class_addMethod, class_addProperty和class_addProtocol允许重建classes。
- class_copyIvarList, class_copyMethodList, class_copyProtocolList和class_copyPropertyList 能拿到一个class的所有内容
- class_getClassMethod, class_getClassVariable, class_getInstanceMethod, class_getInstanceVariable, class_getMethodImplementation和class_getProperty 返回单个内容。
- class_conformsToProtocol, class_respondsToSelector, class_getSuperclass
- 可以使用class_createInstance来创建一个object。
ivar
这些方法能让你得到名字,内存地址和Objective-C type encoding。
method
这些方法主要用来自省,比如method_getName, method_getImplementation, method_getReturnType等等。也有一些修改的方法,包括method_setImplementation和method_exchangeImplementations
objc
一旦拿到了object,你就可以对它做一些自省和修改。你可以get/set ivar, 使用object_copy和object_dispose来copy和free object的内存。
property
属性保存了很大一部分信息。除了拿到名字,你还可以使用property_getAttributes来发现property的更多信息,如返回值、是否为atomic、getter/setter名字、是否为dynamic、背后使用的ivar名字、是否为弱引用。
protocol
Protocols有点像classes,但是精简版的,运行时的方法是一样的。你可以获取method, property, protocol列表, 检查是否实现了其他的protocol。
sel
最后我们有一些方法可以处理 selectors,比如获取名字,注册一个selector等等。
动态特性例子
运行时加载新类
在运行时创建一个新类,只需要3步:
- 为 class pair分配存储空间 ,使用 objc_allocateClassPair函数
- 增加需要的方法使用class_addMethod函数,增加实 例变量用class_addIvar
- 用objc_registerClassPair函数注册这个类,以便它能被别人使用。
字符串与类型转换和判断
通过String来生成Classes和Selectors,可以得知是否存在某个class,NSClassFromString 会返回nil,如果运行时不存在该class的话。
比如可以检查NSClassFromString(@"NSRegularExpression")
是否为nil来判断是否为iOS4.0+。
另一个使用场景是根据不同的输入返回不同的class或method。比如你在解析一些数据,每个数据项都有要解析的字符串以及自身的类型(String,Number,Array)。你可以在一个方法里搞定这些,也可以使用多个方法。
其中一个方法是获取type,然后使用if来调用匹配的方法。
- (void)parseObject:(id)object {
for (id data in object) {
if ([[data type] isEqualToString:@"String"]) {
[self parseString:[data value]];
} else if ([[data type] isEqualToString:@"Number"]) {
[self parseNumber:[data value]];
} else if ([[data type] isEqualToString:@"Array"]) {
[self parseArray:[data value]];
}
}
}
另一种是根据type来生成一个selector,然后调用之。以下是两种实现方式:
- (void)parseObjectDynamic:(id)object {
for (id data in object) {
[self performSelector:NSSelectorFromString([NSString stringWithFormat:@"parse%@:", [data type]]) withObject:[data value]];
}
}
- (void)parseString:(NSString *)aString {}
- (void)parseNumber:(NSString *)aNumber {}
- (void)parseArray:(NSString *)aArray {}
Method Swizzling
Selector相当于一个方法的id;IMP是方法的实现。这样分开的一个便利之处是selector和IMP之间的对应关系可以被改变。比如一个 IMP 可以有多个 selectors 指向它。
Method Swizzling 可以交换两个方法的实现。
Objective-C中,两种扩展class的途径:首先是 subclassing。你可以重写某个方法,调用父类的实现,这也意味着你必须使用这个subclass的实例,但如果继承了某个Cocoa class,而Cocoa又返回了原先的class(比如 NSArray)。这种情况下,你会想添加一个方法到NSArray,也就是使用Category。99%的情况下这是OK的,但如果你重写了某个方法,就没有机会再调用原先的实现了。
Method Swizzling 可以搞定这个问题。你可以重写某个方法而不用继承,同时还可以调用原先的实现。通常的做法是在category中添加一个方法(当然也可以是一个全新的class)。可以通过method_exchangeImplementations这个运行时方法来交换实现。
演示了如何重写addObject:
方法来纪录每一个新添加的对象。
#import <objc/runtime.h>
@interface NSMutableArray (LoggingAddObject)
- (void)logAddObject:(id)aObject;
@end
@implementation NSMutableArray (LoggingAddObject)
+ (void)load {
Method addobject = class_getInstanceMethod(self, @selector(addObject:));
Method logAddobject = class_getInstanceMethod(self, @selector(logAddObject:));
method_exchangeImplementations(addObject, logAddObject);
}
- (void)logAddObject:(id)aobject {
[self logAddObject:aObject];
NSLog(@"Added object %@ to array %@", aObject, self);
}
@end
我们把方法交换放到了load中,这个方法只会被调用一次,而且是运行时载入。如果指向临时用一下,可以放到别的地方。注意到一个很明显的递归调用logAddObject:。这也是Method Swizzling容易把我们搞混的地方,因为我们已经交换了方法的实现,所以其实调用的是addObject:
动态方法处理
当你发送了一个object无法处理的消息时会发生什么呢?很明显,”it breaks”。
大多数情况下确实如此,但Cocoa和runtime也提供了一些应对方法。 首先是动态方法处理。通常来说,处理一个方法,运行时寻找匹配的selector然后执行之。有时,你只想在运行时才创建某个方法,比如有些信息只有在运行时才能得到。
要实现这个效果,你需要重写+resolveInstanceMethod:
或 +resolveClassMethod:
。如果确实增加了一个方法,记得返回YES。
+ (BOOL)resolveInstanceMethod:(SEL)aSelector {
if (aSelector == @selector(myDynamicMethod)) {
class_addMethod(self, aSelector, (IMP)myDynamicIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSelector];
}
消息转发
如果 resolve method 返回NO,运行时就进入下一步骤:消息转发。
有两种常见用例:
- 将消息转发到另一个可以处理该消息的object。
- 将多个消息转发到同一个方法。
消息转发分两步
- 首先,运行时调用
-forwardingTargetForSelector:
,如果只是想把消息发送到另一个object,那么就使用这个方法,因为更高效。如果想要修改消息,那么就要使用-forwardInvocation:
,运行时将消息打包成NSInvocation,然后返回给你处理。 - 处理完之后,调用
invokeWithTarget:
。
Cocoa有几处地方用到了消息转发,主要的两个地方是代理(Proxies)和响应链(Responder Chain)。NSProxy是一个轻量级的class,它的作用就是转发消息到另一个object。如果想要惰性加载object的某个属性会很有用。NSUndoManager也有用到,不过是截取消息,之后再执行,而不是转发到其他的地方。
使用Block作为Method IMP
通常一个 IMP 是一个指向方法实现的指针,头两个参数为 object(self)和selector(_cmd)。
imp_implementationWithBlock() 能让我们使用block作为 IMP,下面这个代码片段展示了如何使用block来添加新的方法。
IMP myIMP = imp_implementationWithBlock(^(id _self, NSString *string) {
NSLog(@"Hello %@", string);
});
class_addMethod([MYclass class], @selector(sayHello:), myIMP, "v@:@");
参考
- https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html
- https://developer.apple.com/reference/objectivec/1657527-objective_c_runtime
- https://tangjr.gitbooks.io/objective-c-tutorials/content/
- http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
- https://www.zybuluo.com/MicroCai/note/64270
- http://www.saitjr.com/ios/objc-runtime.html
- https://onevcat.com/2012/04/objective-c-runtime/
- http://blog.csdn.net/goodlixueyong/article/details/40820075
- http://www.cocoachina.com/industry/20130819/6824.html
- http://www.cnblogs.com/huangtianhui/archive/2013/04/18/3029166.html