Runtime基礎(chǔ)元素解析

Objective-C的runtime語言使它具備了動態(tài)語言的特性,也就是平時所說的“運行時”。在runtime的基礎(chǔ)上,可以做很多平時難以想到事,或者化簡原先 較為繁雜的解決方案。

相對于靜態(tài)語言,比如C的以下程序

#include 
void run()
{}
int main()
{
    return 0;
}

執(zhí)行clang -c進行編譯后,獲取符號表nm run.o,可以得到全局唯一的符號_run,對函數(shù)run的調(diào)用直接參考鏈接后_run符號在代碼段的地址

0000000000000010 T _main
0000000000000000 T _run

對比Objective-C的以下函數(shù)

@implementation Dog : NSObject
- (void)run
{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = [[Dog alloc] init];
        [dog run];
    }
    return 0;
}

執(zhí)行clang -rewrite-objc main.m將其轉(zhuǎn)換成底層C++文件后可以得到

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run"));
    }
    return 0;
}

可以看到,對Objective-C編譯前期,會將內(nèi)部的方法調(diào)用,轉(zhuǎn)換成調(diào)用objc_msgSend。也就是說,編譯完成后,方法地址是不能確定的,需要在運行時,通過Selector進行查找,而這正是runtime的關(guān)鍵,也就是發(fā)送消息機制。

runtime的基本要素

如上面例子所示,在編譯后[dog run]被編譯器轉(zhuǎn)化成了

((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run"));

// 假設(shè)能省略(void (*)(id, SEL))(void *)和id指針強轉(zhuǎn)[實際上還是需要的]
// sel_registerName表示注冊一個selector
objc_msgSend(dog, sel_registerName("run"));

將上面的情況抽取成統(tǒng)一的說法就是,在編譯器編譯后[receiver message]會被轉(zhuǎn)化成以下形式

objc_msgSend(receiver, selector)

objc_msgSend是一個消息發(fā)送函數(shù),它以消息接收者和方法名作為基礎(chǔ)參數(shù)。

在有參數(shù)的情況下,則會被轉(zhuǎn)換為

objc_msgSend(receiver, selector, arg1, arg2, ...)

消息的接收者receiver在接受到消息后,查找對應(yīng)selector的實現(xiàn),根據(jù)查找的結(jié)果可以進行若干種種不同的處理。

更深層的了解,需要了解下對應(yīng)的數(shù)據(jù)結(jié)構(gòu)

id

上文中objc_msgSend的第一個參數(shù)有個強轉(zhuǎn)類型,即id。id是可以指向?qū)ο蟮娜f能指針,查看runtime源碼,得知其定義如下:

typedef struct objc_object *id;

// objc_object
struct objc_object {
private:
    isa_t isa;
}

// isa_t
union isa_t
{
    Class cls;
    uintptr_t bits;
}

根據(jù)union聯(lián)合的存儲空間以大成員的存儲空間計算性質(zhì),可以猜測isa_t的作用只是真不同位數(shù)處理器的優(yōu)化,我們可以直接這樣表示:

struct objc_object {
private:
    Class isa;
}

可以看出,id是一個指向objc_object結(jié)構(gòu)體的指針(注意,在runtime中對象可以用結(jié)構(gòu)體進行表示)。

objc_object結(jié)構(gòu)體包含了Class isa成員,而isa就是我們常說的創(chuàng)建一個對象時,用來指向所屬類的指針。因此根據(jù)isa就可以獲取對應(yīng)的類。

  • 注:C++中結(jié)構(gòu)的作用被拓寬了,也表示定義一個類的類型,struct和class的區(qū)別就在默認類型上一個是public,一個是private,這里就直接描述為結(jié)構(gòu)體了

Class

上文中,isaClass類型,而Class則是objc_class指針類型的別名:

typedef struct objc_class *Class;

objc_class具體的定義如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

// class_data_bits_t
struct class_data_bits_t {
    ...
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    ...
}

// class_rw_t
struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    union {
        method_list_t **method_lists;  // RW_METHOD_ARRAY == 1
        method_list_t *method_list;    // RW_METHOD_ARRAY == 0
    };
    struct chained_property_list *properties;
    const protocol_list_t ** protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

// class_ro_t
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;

    const char * name;
    const method_list_t * baseMethods;
    const protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    const property_list_t *baseProperties;
};

在上文中已經(jīng)介紹過objc_object結(jié)構(gòu)體,objc_class繼承自結(jié)構(gòu)體objc_object??梢钥闯?code>objc_object的isaprivate類型成員變量,objc_class繼承后無法訪問,所以objc_object提供了以下兩個成員函數(shù):

Class ISA();

// getIsa內(nèi)部調(diào)用ISA返回isa_t聯(lián)合中cls成員
Class getIsa();

所以,對objc_class重要的成員變量進行下解釋:

  • isa為指向?qū)ο髮?yīng)類的指針(這里注意一點,由于類也是一個對象(單例),所以這個單例中也有一個isa指針指向類對象所屬的類->metaClass,即元類)

  • superclass為指向父類的指針

  • cache用于對調(diào)用方法的緩存,類似CPU先訪問L1、L2、L3緩存的目的相似,它也是推斷最近調(diào)用的方法極有可能被二次調(diào)用,并將其存入cache,在二次調(diào)用時先在cache查找方法,而不是直接在類的方法列表中查找

  • properties為屬性列表

  • protocols為協(xié)議列表

  • method_lists/method_list為方法列表

  • ivars為成員變量列表

  • class_ro_t結(jié)構(gòu)體中存儲的都是類基本的東西,比如獲取'load'方法時,是從baseMethods獲取相應(yīng)的IMP函數(shù)實現(xiàn)的:

IMP objc_class::getLoadMethod()
{
rwlock_assert_locked(&runtimeLock);

const method_list_t *mlist;
uint32_t i;

assert(isRealized());
assert(ISA()->isRealized());
assert(!isMetaClass());
assert(ISA()->isMetaClass());

mlist = ISA()->data()->ro->baseMethods;
if (mlist) {
    for (i = 0; i < mlist->count; i++) {
        method_t *m = method_list_nth(mlist, i);
        const char *name = sel_cname(m->name);
        if (0 == strcmp(name, "load")) {
            return m->imp;
        }
    }
}

return nil;

}


其中先了解下`ivar_list_t`、`method_list_t`、`cache_t`的結(jié)構(gòu)定義:


`ivar_list_t`的結(jié)構(gòu)為:

  - `ivar_t`就是對應(yīng)的成員變量

```objc
struct ivar_list_t {
    uint32_t entsize;
    uint32_t count;
    ivar_t first;
};

method_list_t為:

  • 其中method_iterator為結(jié)構(gòu)體自己構(gòu)造的一個迭代器,用來訪問方法,可以看到,構(gòu)造的迭代器結(jié)構(gòu)體中包含了method成員變量
struct method_list_t {
    uint32_t entsize_NEVER_USE;  // high bits used for fixup markers
    uint32_t count;
    method_t first;

    // iterate methods, taking entsize into account
    // fixme need a proper const_iterator
    struct method_iterator {
        uint32_t entsize;
        uint32_t index;  // keeping track of this saves a divide in operator-
        method_t* method;
    ...
    }

cache_t為:

  • 可以看出bucket_t包含了一個IMP類型的私有成員,供查找后調(diào)用實現(xiàn)
  • _occupied_mask分別表示實際占用的緩存_buckets總數(shù)和分配的緩存_buckets總數(shù)
struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
...
}

// bucket_t
struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
...
}

上文還涉及到了一個概念metaClass元類,元類為類對象所屬的類,以實例解釋:

當(dāng)我們調(diào)用類方法時,消息的接收者即為類,如文中一開始的代碼:

Dog *dog = [[Dog alloc] init];

這里的alloc消息即發(fā)送給了Dog類,編譯轉(zhuǎn)換后的代碼為:

Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));

我們只需要關(guān)注這一行:

  • 這里獲取到的是類對象,只要再獲取一次就得到了元類
// objc_getClass表示根據(jù)對象名獲取對應(yīng)的類
objc_getClass("Dog")

// 獲取元類
objc_getClass(objc_getClass("Dog"))

關(guān)于元類,蘋果提供了這么一張表:

對象-類-元類-超類

圖中的實線是superclass指針,虛線是isa指針??梢钥吹剑惖某?code>NSObject(Root class)并沒有對應(yīng)的超類,并且,它的isa指針指向了自己。
總結(jié)一下:

  • 每個實例對象的isa都指向了所屬的
  • 每個類對象的isa都指向了所屬的類,即元類,其superclass指針指向繼承的父類
  • 每個元類的isa都指向了超類,即NSObject

Ivar

Ivar,我把它理解成instance variable,也就是實例變量,可以觀察它的定義:

typedef struct ivar_t *Ivar;

// ivar_t
struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;
    // 內(nèi)存中數(shù)據(jù)對齊(如字對齊、半字對齊等)
    uint32_t alignment() {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};

Ivar其實是指向ivar_t結(jié)構(gòu)體的指針,它包含了實例變量名(name)、類型(type)、相對對象地址偏移(offset)以及內(nèi)存數(shù)據(jù)對齊等信息。

跟多關(guān)于實例變量的剖析可以查看Objective-C類成員變量深度剖析

Method

從以下定義的結(jié)構(gòu)體可以看出,Method主要住用為關(guān)聯(lián)了方法名SEL和方法的實現(xiàn)IMP,當(dāng)遍通過Method自己的定義的迭代器查找方法名SEL時,就可以找到對應(yīng)的方法實現(xiàn)IMP,從而調(diào)用方法的實現(xiàn)執(zhí)行相關(guān)的操作。types表示方法實現(xiàn)的參數(shù)以及返回值類型。

typedef struct method_t *Method;

// method_t
struct method_t {
    SEL name;
    const char *types;
    IMP imp;
    ...
}

SEL

SEL為方法選擇器,觀察下它的定義:

typedef struct objc_selector *SEL;

可以看出SEL實際是objc_selector指針類型的別名,它用于表示運行時方法的名字,以便進行方法實現(xiàn)的查找。因為要對應(yīng)方法實現(xiàn),所以每一個方法對應(yīng)的SEL都是唯一的。因此它不具備C++可以進行函數(shù)重載的特性,當(dāng)兩個方法名一樣時,會發(fā)生編譯錯誤,即使參數(shù)不一樣。

IMP

IMP的定義如下:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif

可以看出IMP其實就是一個函數(shù)指針的別名,也可以把它理解為函數(shù)名。它有兩個必須的參數(shù):

  • id,為self指針,表示消息接收者
  • SEL,方法選擇器,表示一個方法的selector指針
  • 后面的為傳送消息的一些參數(shù)

在某些情況下,通過獲取IMP而直接調(diào)用方法實現(xiàn),可以直接跳過消息傳遞機制,像C語言調(diào)用函數(shù)那樣,在一定程度上,可以提供程序的性能。

消息傳遞

了解完runtime中一些必要的元素,繼續(xù)回到文章開頭的代碼:

@implementation Dog : NSObject
- (void)run
{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = [[Dog alloc] init];
        [dog run];
    }
    return 0;
}

編譯器將其轉(zhuǎn)換成了:

  • 為了看起來簡潔點,我把一些強制轉(zhuǎn)換變?yōu)閯e名
typedef (Dog *(*)(id, SEL))(void *) MyImp;

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        Dog *dog = ((MyImp)objc_msgSend)((id)((MyImp)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
        ((MyImp)objc_msgSend)((id)dog, sel_registerName("run"));
    }
    return 0;
}

從上面的代碼可以看出,第二個objc_msgSend返回值是作為第一個objc_msgSend的首個參數(shù)的。

上文已經(jīng)說過,[receiver message]會被轉(zhuǎn)化成以下形式

objc_msgSend(receiver, selector, ...)

接下來看看它主要做了哪幾件事情:

  • 根據(jù)receiverisa指針,獲取到所屬類,先在類的cache即緩存中查找selector,如果沒有找到,再在類的method_lists即方法列表中查找
  • 如果沒有找到selector,則會沿著下圖類的聯(lián)系路徑一直查找,直到NSObject
  • 如果找到了selector,則獲取實現(xiàn)方法并調(diào)用,并傳入接收者對象以及方法的所有參數(shù);沒有找到時走方法解析和消息轉(zhuǎn)發(fā)流程。
  • 將實現(xiàn)的返回值作為它自己的返回值

方法列表查找路徑

除此之外,objc_msgSend還會傳遞兩個隱藏參數(shù):

  • 消息接收對象(self引用的對象)
  • 方法選擇器(_cmd,調(diào)用的方法)

objc_msgSend找到方法實現(xiàn)后,會在調(diào)用該實現(xiàn)時,傳入這兩個隱藏參數(shù),這樣就能夠在方法實現(xiàn)里面里面獲取消息接受對象,即方法調(diào)用者了。

隱藏參數(shù)表示這兩個參數(shù)在源代碼方法的定義中并沒有聲明這兩個參數(shù),這兩個參數(shù)是在代碼編譯期間,被插入到實現(xiàn)中的。

self和super的聯(lián)系

根據(jù)上文對objc_msgSend的了解,可以解決以下代碼輸出一致問題

@implementation Dog : NSObject

- (void)run
{
    NSLog(@"%@", [self class]);
    NSLog(@"%@", [super class]);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = [[Dog alloc] init];

        [dog run];
    }
    return 0;
}

輸出為:

[5491:173185] Dog
[5491:173185] Dog

這是為什么呢?先來看看編譯后的-run方法的情況:

static void _I_Dog_run(Dog * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_50_3f5nr6h10h1csn8byghy30q80000gn_T_main_d06ff4_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_50_3f5nr6h10h1csn8byghy30q80000gn_T_main_d06ff4_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }, sel_registerName("class")));
}

這里面只要關(guān)注兩句:

// [self class]
((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))

// [super class]
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }, sel_registerName("class"))

首先我們需要了解selfsuper的差異:

  • super編譯標(biāo)識符,告訴編譯器,調(diào)用方法時,去調(diào)用父類的方法,而不是本類的方法
  • self隱藏參數(shù),每個方法的實現(xiàn)第一個參數(shù)就是self

這里可以看出,編譯后,經(jīng)過super標(biāo)識符修飾的方法調(diào)用,會調(diào)用objc_msgSendSuper函數(shù)來進行消息的發(fā)送,而不是objc_msgSend。先來了解下objc_msgSendSuper的聲明:

id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );

其中objc_super的定義為:

// receiver   消息實際接收者
// class      指向當(dāng)前類的父類
struct objc_super { id receiver; Class class; };

結(jié)合以上信息,我們可以知道:

(__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }

就是對結(jié)構(gòu)體objc_super的賦值,也就是說objc_super->receiver=self。到這里可能就有點明了了,super只是告訴編譯器,去查找父類中的class方法,當(dāng)找到之后,使用objc_super->receiverself進行調(diào)用。用流程表示就是:

[super class]->objc_msgSendSuper(objc_super{self, superclass)}, sel_registerName("class"))->objc_msgSend(objc_super->self, sel_registerName("class"))=[self class]

可以看出兩者輸出結(jié)果一致的關(guān)鍵就是,[self class]的消息接收者和[super class]的消息接收者一樣,都是調(diào)用方法的實例對象。

方法解析和消息轉(zhuǎn)發(fā)

當(dāng)上文objc_msgSend處理流程中,selector沒有找到時,會觸發(fā)三個階段,在這三個階段都可以進行相關(guān)處理使程序不拋出異常:

  • Method Resolution (動態(tài)方法解析)
  • Fast Forwarding (備用接收者)
  • Normal Forwarding (完整轉(zhuǎn)發(fā))

由于實際代碼中很少有看到這種操作,所以這里不做詳細解釋,參考這個資料即可Objective-C Runtime 運行時之三:方法與消息

參考

1.Objective-C Runtime 運行時之一:類與對象

2.Objective-C Runtime

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,041評論 0 9
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,248評論 0 9
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 828評論 0 2
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,881評論 33 466
  • 貪念 是你此生結(jié)下的緣 不知何時是何緣由你貪婪成性 不知何地你貪婪成名 貪念 是你燃燒的欲望之火 讓你愛上人間這個...
    藍色汪星人閱讀 410評論 0 2

友情鏈接更多精彩內(nèi)容