Objective-C是支持比较丰富的动态语言特性,能运行时修改类型,并支持类型的自省,相对于C++之类的静态语言,运行时自省是非常灵活自由的特性。

动态特性基础

包含头文件 #import <objc/runtime.h>

id类型

通用指针类型,弱类型,编译时不进行类型检查

– 如果不涉及到多态,尽量使用静态类型 – 静态类型可更好的在编译阶段而不是运行阶段指 出错误 – 静态类型能够提高程序的可读性

NSObject的动态性方法

NSObject还有些方法能在运行时获得类的信息,并检查一些特性:

返回对象的类

返回父类的Class

检查对象是否是指定的类或其派生类的实例

检查对象是否是指定的类的实例

检查对象能否响应指定的消息

检查对象是否实现了指定协议类的方法

则返回指定方法实现的地址

例子:

@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类型

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类型,在运行时候是通过方法的标示来查找方法的。

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)。

消息是通过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

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步:

  1. 为 class pair分配存储空间 ,使用 objc_allocateClassPair函数
  2. 增加需要的方法使用class_addMethod函数,增加实 例变量用class_addIvar
  3. 用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,运行时就进入下一步骤:消息转发。

有两种常见用例:

  1. 将消息转发到另一个可以处理该消息的object。
  2. 将多个消息转发到同一个方法。

消息转发分两步

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@:@");

参考