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大有用武之地。讓我們接下去一起慢慢發掘吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM