ObjC之RunTime(上)

转载自这里

最近看了一本书——iOS6 programming Pushing the Limits(亚马逊有中文版),最后一章是关于Deep ObjC的,主要内容是ObjC的runtime。虽然之前看过runtime的programming guide,但读之乏味也不知道能用在何处。现在有点小小的理解,觉得别有乾坤,索性把runtime的相关东西给整理一下。 下面就从官方文档开始,看看runtime有哪些特性,以及各自的应用场合。

基本概念

对于现在绝大多数的64位操作系统而言,我们接触到的都是ObjC2.0的modern runtime。ObjC程序从3个层次来使用到runtime:

1.ObjC源码

这说明了runtime是ObjC的基石,你定义的类/方法/协议等等,最后都需要使用到runtime。其中,最重要的部分就是方法的messaging。

2.ObjC方法(Method)

绝大多数ObjC都继承自NSObject,他们都可以在运行的时候检查属于/继承哪个类,某个对象是否有某个方法,是否实现了某个协议等等。这一部分是编程时,经常会使用到的。

3.ObjC函数(Function)

Runtime相关的头文件在: /usr/include/objc中,我们可以使用其中定义的对象和函数。通常情况下,我们很少会使用到。但个别情况我们可能需要使用,比如swizzling。此外,这些纯C的实现说明了我们可以用C来实现ObjC的方法。

Messaging

之前说过,所有的ObjC方法最后都通过runtime实现,这都是通过调用函数objc_msgSend. 也就是说诸如: [receiver doSomething] 的调用最终都是展开调用objc_msgSend完成的。 在此之前,先看下ObjC的class定义:

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;

因为现在的objc是2.0,所以上述的Class可以简化为:

struct objc_class {
    Class isa;
}

Class只是一个包含了指向自身结构体的isa指针的结构体,虽然这个结构体具体的内容没有找到定义,但是根据头文件里的写法我们可以猜测,它必定还包含父类,变量,方法,协议等信息(最新的runtime信息可以在opensource中查看)。 而objc_msgSend定义在Message.h文件里:

id objc_msgSend(id theReceiver, SEL theSelector, ...)
  • theReceiver: 处理该消息的对象
  • theSelector: 处理该消息的方法
  • ...: 消息需要的参数
  • id: 消息完成后的返回值。

文档中提到:

When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

从函数类型和说明可以知道,最关键的就是要获得selector。Selector本质上是一个函数指针,有了这个指针就能执行相应的程序。当某一个对象实例化后,首先通过isa指针来访问自身Class的信息,寻找相应的selector的地址。如果找不到,那就可以通过指向父类的指针遍历父类的selector的地址,如此这般,直到根类,如下图:

Messaging Framework

大致原理就是如此,当然为了提高速度,objc_msgSend是做了很多优化的。知道了这些,我们就可以自己实现一个objc_msgSend,所需要的关键无非是:调用对象,执行函数(获得函数指针的地址即可),以及相应的参数。iOS6PTL最后部分有相应的说明,这里就不多说,把代码发出来:

//MyMsgSend.c
#include <stdio.h>
#include <objc/runtime.h>
#include "MyMsgSend.h"

static const void *myMsgSend(id receiver, const char *name) {
  SEL selector = sel_registerName(name);
  IMP methodIMP =
  class_getMethodImplementation(object_getClass(receiver),
                                selector);
  return methodIMP(receiver, selector);
}

void RunMyMsgSend() {
  // NSObject *object = [[NSObject alloc] init];
  Class class = (Class)objc_getClass("NSObject");
  id object = class_createInstance(class, 0);
  myMsgSend(object, "init");

  // id description = [object description];
  id description = (id)myMsgSend(object, "description");

  // const char *cstr = [description UTF8String];
  const char *cstr = myMsgSend(description, "UTF8String");

  printf("%s\n", cstr);
}

 方法的动态实现(Dynamic Method Resolution)

有了上面的基础,我们就很容易给类在runtime添加方法。比如,objc中有dynamic的属性关键字(使用过coredata的都知道),这个就提示该属性的方法在运行时提供。在运行时添加方法,只要实现:

+ (BOOL)resolveInstanceMethod:(SEL)sel
//相应的也存在+ (BOOL)resolveClassMethod:(SEL)sel
{
    DLog(@"");
    if (sel == @selector(xxx))
    {
        class_addMethod(.....);
        return YES;
    }

    return [super resolveInstanceMethod:sel];
}

在调用的时候使用 performSelector:方法,或者直接调用某个定义过但是没有实现的方法,resolveInstanceMethod都会被出发进行方法查找,下图是运行时的调用栈信息: 

可以看到runtime依次调用了两个函数来查找selector,当它在类以及父类中没有找到时,就会调用resolveInstanceMethod。

动态加载(Dynamic Loading)

(这部分主要侧重于Mac OS 系统) 我们知道category是在第一次使用到的时候添加到class的,因此objc也提供了动态添加class的机制。比如OS的系统偏好里的一些设置就是通过动态添加实现的,当然还有插件系统。 runtime提供了相应的函数(objc/objc-load.h),但对于cocoa系统,我们可以使用NSBundle来更好的操作。下面简单的说一下步骤:

  1. 新建一个cocoa的工程,选择bundle模板;
  2. 新建一个class,然后添加一个方法并实现之;
  3. 修改plist文件,在principle class一行将新建的class名填进去;
  4. build工程,然后在Finder里找到bundle;
  5. 新建一个测试bundle的工程,模板任选(可以选择application)
  6. 把之前的bundle文件添加的测试工程,然后添加相应的代码:
    - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
    {
        // Insert code here to initialize your application
    
        NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"DynamicClassBundle" ofType:@"bundle"];
        NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];
        if (bundle)
        {
            Class principleClass = [bundle principalClass];
            if (principleClass)
            {
                id bundleInstance = [[principleClass alloc] init];
                [bundleInstance performSelector:@selector(print) withObject:nil withObject:nil];
            }
        }
    }

消息路由(Message Forwarding)

向一个对象发送未定义的消息时,程序往往会奔溃。其实,在崩溃前,runtime还做了一些工作:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
}

使用forwardInvocation的话,上述两个方法都要实现。runtime先寻找是否存在方法签名(NSMethodSignature),如果找到了再去执行forwardInvocation。注意在这里,消息的参数(假设存在的话)没有出现,这就说明被runtime通过某种方式保存起来了。当然我们可以通过获得的NSInvocation来修改。 这是常规的消息路由方式,runtime也提供了“捷径”:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
}

这种方式可以直接把消息传递给需要(能够)处理的对象,而且这种方式比上述forwardInvocation要快,引用文档的话说:

This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.

可见单纯转发可以用这种方式,但是如果要纪录NSInvocation或者改变参数之类的,就要用forwardInvocation。 消息转发模拟了多继承(ObjC本身是不支持多继承),可以在子类调用父类的父类的实现;当然也提供了调用任意类的方法的途径。Cocoa中有Distributed Object就利用了这种特性,它可以在一个application中使用另一个application(甚至是运行在同一网络中不同电脑上的application)中定义的对象。这部分暂时放一放,有兴趣的可以深入。

类型编码(Type Encodings)

看一下动态添加方法到类的函数:

class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)

注意最后一个参数,为了支持runtime,编译器需要知道每一个参数的类型,因此预先定义了相应的字符。这个types所代表的意思的含义依次是:

返回值,receiver类型,SEL,参数1,。。。参数n

具体的类型定义参见官方文档,由此我们可以得知该参数的第二和第三为参数必定是"@:"。

属性声明(Declare Properties)

如果可以在runtime的时候获得类的属性,这将会很有用处,比如对json数据序列化。runtime提供了相应的函数来实现:

unsigned int propertyCount = 0;
    objc_property_t *propertyArray = class_copyPropertyList([MyClass class], &propertyCount);
    NSLog(@"property of MyClass:");
    for (int i = 0; i < propertyCount; i++)
    {
        objc_property_t property = propertyArray[i];
        fprintf(stdout, "%s : %s\n",property_getName(property),property_getAttributes(property));
    }

    propertyCount = 0;
    propertyArray = class_copyPropertyList([MyChildClass class], &propertyCount);
    NSLog(@"property of MyChildClass:");
    for (int i = 0; i < propertyCount; i++)
    {
        objc_property_t property = propertyArray[i];
        fprintf(stdout, "%s : %s\n",property_getName(property),property_getAttributes(property));
    }

runtime只会获取当前类的属性——父类的以及扩展里实现的属性都不能通过这样的方式获取。property_getAttributes获得的属性的“属性”会以如下的形式:

T<类型>,Attribute1,...AttributeN,V_propertyName

其中的Attibute是属性的类型编码,具体的在官方文档。 这些就是runtime的基本内容,好像有点枯燥,平时也不怎么用的上。最初我也觉得是,不过隐约的感觉runtime大有用武之地。让我们接下去一起慢慢发掘吧。

posted on 2013-10-22 23:06  scorpiozj  阅读(1375)  评论(5编辑  收藏