Objective-C 的动态提示和技巧

过去的几年中涌现了大量的Objective-C开发者。有些是从动态语言转过来的,好比Ruby或Python,有些是从强类型语言转过来的,如Java或C#,固然也有直接以Objective-C做为入门语言的。也就是说有很大一部分开发者都没有使用Objective-C太长时间。当你接触一门新语言时,更多地会关注基础知识,如语法和特性等。但一般有一些更高级的,更不为人知又有强大功能的特性等待你去开拓。html

The Runtimepython

Objective-C是一门简单的语言,95%是C。只是在语言层面上加了些关键字和语法。真正让Objective-C如此强大的是它的运行时。它很小但却很强大。它的核心是消息分发。ios

Messages安全

若是你是从动态语言如Ruby或Python转过来的,可能知道什么是消息,能够直接跳过进入下一节。那些从其余语言转过来的,继续看。app

执行一个方法,有些语言,编译器会执行一些额外的优化和错误检查,由于调用关系很直接也很明显。但对于消息分发来讲,就不那么明显了。在发消息前没必要知道某个对象是否可以处理消息。你把消息发给它,它可能会处理,也可能转给其余的Object来处理。一个消息没必要对应一个方法,一个对象可能实现一个方法来处理多条消息。优化

在Objective-C中,消息是经过objc_msgSend()这个runtime方法及相近的方法来实现的。这个方法须要一个target,selector,还有一些参数。理论上来讲,编译器只是把消息分发变成objc_msgSend来执行。好比下面这两行代码是等价的。atom

[array insertObject:foo atIndex:5];
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

大多数面向对象的语言里有 classes 和 objects 的概念。Objects经过Classes生成。可是在Objective-C中,classes自己也是objects(译者注:这点跟python很像),也能够处理消息,这也是为何会有类方法和实例方法。具体来讲,Objective-C中的Object是一个结构体(struct),第一个成员是isa,指向本身的class。这是在objc/objc.h中定义的。spa

typedef struct objc_object {
    Class isa;
} *id;

object的class保存了方法列表,还有指向父类的指针。但classes也是objects,也会有isa变量,那么它又指向哪儿呢?这里就引出了第三个类型: metaclasses。一个 metaclass被指向class,class被指向object。它保存了全部实现的方法列表,以及父类的metaclass。若是想更清楚地了解objects,classes以及metaclasses是如何一块儿工做地,能够阅读这篇文章代理

Methods, Selectors and IMPs指针

咱们知道了运行时会发消息给对象。咱们也知道一个对象的class保存了方法列表。那么这些消息是如何映射到方法的,这些方法又是如何被执行的呢?

第一个问题的答案很简单。class的方法列表实际上是一个字典,key为selectors,IMPs为value。一个IMP是指向方法在内存中的实现。很重要的一点是,selector和IMP之间的关系是在运行时才决定的,而不是编译时。这样咱们就能玩出些花样。

IMP一般是指向方法的指针,第一个参数是self,类型为id,第二个参数是_cmd,类型为SEL,余下的是方法的参数。这也是self和_cmd被定义的地方。下面演示了Method和IMP

- (id)doSomethingWithInt:(int)aInt{}
 
id doSomethingWithInt(id self, SEL _cmd, int aInt){}


其余运行时的方法

如今咱们知道了objects,classes,selectors,IMPs以及消息分发,那么运行时到底能作什么呢?主要有两个做用:

1.建立、修改、自省classes和objects

2.消息分发

以前已经提过消息分发,不过这只是一小部分功能。全部的运行时方法都有特定的前缀。下面是一些有意思的方法:

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的内存。最NB的不只是拿到一个class,而是可使用object_setClass来改变一个object的class。待会就能看到使用场景。

property

属性保存了很大一部分信息。除了拿到名字,你还可使用property_getAttributes来发现property的更多信息,如返回值、是否为atomic、getter/setter名字、是否为dynamic、背后使用的ivar名字、是否为弱引用。

protocol

Protocols有点像classes,可是精简版的,运行时的方法是同样的。你能够获取method, property, protocol列表, 检查是否实现了其余的protocol。

sel

最后咱们有一些方法能够处理 selectors,好比获取名字,注册一个selector等等。

如今咱们对Objective-C的运行时有了大概的了解,来看看它们能作哪些有趣的事情。

Classes And Selectors From Strings

比较基础的一个动态特性是经过String来生成Classes和Selectors。Cocoa提供了NSClassFromString和NSSelectorFromString方法,使用起来很简单:

Class stringclass = NSClassFromString(@"NSString");

因而咱们就获得了一个string class。接下来:

NSString *myString = [stringclass stringWithString:@"Hello World"];

为何要这么作呢?直接使用Class不是更方便?一般状况下是,但有些场景下这个方法会颇有用。首先,能够得知是否存在某个class,NSClassFromString 会返回nil,若是运行时不存在该class的话。好比能够检查NSClassFromString(@"NSRegularExpression")是否为nil来判断是否为iOS4.0+。

另外一个使用场景是根据不一样的输入返回不一样的class或method。好比你在解析一些数据,每一个数据项都有要解析的字符串以及自身的类型(String,Number,Array)。你能够在一个方法里搞定这些,也可使用多个方法。其中一个方法是获取type,而后使用if来调用匹配的方法。另外一种是根据type来生成一个selector,而后调用之。如下是两种实现方式:

- (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]];
        }
    }
}
- (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 {}

可一看到,你能够把7行带if的代码变成1行。未来若是有新的类型,只需增长实现方法便可,而不用再去添加新的 else if。

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这个运行时方法来交换实现。来看一个demo,这个demo演示了如何重写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:

 

动态继承、交换

咱们能够在运行时建立新的class,这个特性用得很少,但其实它仍是很强大的。你能经过它建立新的子类,并添加新的方法。

但这样的一个子类有什么用呢?别忘了Objective-C的一个关键点:object内部有一个叫作isa的变量指向它的class。这个变量能够被改变,而不须要从新建立。而后就能够添加新的ivar和方法了。能够经过如下命令来修改一个object的class

object_setClass(myObject, [MySubclass class]);

这能够用在Key Value Observing。当你开始observing an object时,Cocoa会建立这个object的class的subclass,而后将这个object的isa指向新建立的subclass。点击这里查看更详细的解释。

动态方法处理

目前为止,咱们讨论了方法交换,以及已有方法的处理。那么当你发送了一个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];
}


那Cocoa在什么场景下会使用这些方法呢?Core Data用得不少。NSManagedObjects有许多在运行时添加的属性用来处理get/set属性和关系。那若是Model在运行时被改变了呢?

消息转发

若是 resolve method 返回NO,运行时就进入下一步骤:消息转发。有两种常见用例。1) 将消息转发到另外一个能够处理该消息的object。2) 将多个消息转发到同一个方法。

消息转发分两步。首先,运行时调用-forwardingTargetForSelector:,若是只是想把消息发送到另外一个object,那么就使用这个方法,由于更高效。若是想要修改消息,那么就要使用-forwardInvocation:,运行时将消息打包成NSInvocation,而后返回给你处理。处理完以后,调用invokeWithTarget:。

Cocoa有几处地方用到了消息转发,主要的两个地方是代理(Proxies)和响应链(Responder Chain)。NSProxy是一个轻量级的class,它的做用就是转发消息到另外一个object。若是想要惰性加载object的某个属性会颇有用。NSUndoManager也有用到,不过是截取消息,以后再执行,而不是转发到其余的地方。

响应链是关于Cocoa如何处理与发送事件与行为到对应的对象。好比说,使用Cmd+C执行了copy命令,会发送-copy:到响应链。首先是First Responder,一般是当前的UI。若是没有处理该消息,则转发到下一个-nextResponder。这么一直下去直到找到可以处理该消息的object,或者没有找到,报错。

使用Block做为Method IMP

iOS 4.3带来了不少新的runtime方法。除了对properties和protocols的增强,还带来一组新的以 imp 开头的方法。一般一个 IMP 是一个指向方法实现的指针,头两个参数为 object(self)和selector(_cmd)。iOS 4.0和Mac OS X 10.6 带来了block,imp_implementationWithBlock() 能让咱们使用block做为 IMP,下面这个代码片断展现了如何使用block来添加新的方法。

IMP myIMP = imp_implementationWithBlock(^(id _self, NSString *string) {
    NSLog(@"Hello %@", string);
});
class_addMethod([MYclass class], @selector(sayHello:), myIMP, "v@:@");


若是想知道这是如何实现的,能够查看这篇文章

能够看到,Objective-C 表面看起来挺简单,但仍是很灵活的,能够带来不少可能性。动态语言的优点在于在不扩展语言自己的状况下作不少很灵巧的事情。好比Key Value Observing,提供了优雅的API能够与已有的代码无缝结合,而不须要新增语言级别的特性。咱们知道,Objective-C开发iOS app,不得不质疑的是app安全,关于iOS app安全如何进行保护,能够点击这里查看

相关文章
相关标签/搜索