ObjC之RunTime(上)

scorpiozj發表於2013-10-22

轉載自這裡

最近看了一本書——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大有用武之地。讓我們接下去一起慢慢發掘吧。

相關文章