1. 程式人生 > >iOS開發:Runtime解析

iOS開發:Runtime解析

       在iOS開發過程中,使用Runtime的場景雖然沒有GCD多,但是也是一個不容忽視的模組,而且在面試找工作的時候也是面試官必考題。那麼下面就來了解一下Runtime的相關知識,大牛請繞行,不喜勿噴。

       一、Runtime概念

           1.Runtime,又叫執行時,是一套關於底層的C語言API,為iOS內部的核心之一,我們在編寫OC程式碼的時候,底層都是基於它來實現的。

eg:[receiver message];

底層執行時會被編譯器轉化為:

objc_msgSend(receiver, selector)

如果它還有引數,比如:

[receiver message:(id)arg...];

底層執行時,會被編譯器轉化為:

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

 2.為什麼需要Runtime:

        Objective-C是一門面向物件的動態語言,它會將一些工作放在程式碼執行時才處理而並非編譯時。也就是說,有很多類和成員變數在編譯的時候是不知道的,而在執行時,所編寫的程式碼會轉換成完整的確定的程式碼執行。因此,指望編譯器是遠遠不夠的,還需要一個執行時系統(Runtime system)來處理編譯後的程式碼。Runtime基本是用C和彙編寫的,由此可見蘋果為了動態系統的高效而做出的努力。蘋果和GNU各自維護一個開源的Runtime版本,這兩個版本之間都在努力保持一致。

            3.Runtime的作用:

        Objective-C在三種層面上與Runtime系統進行互動:
        1.通過OC原始碼
        只需要編寫OC程式碼,Runtime系統自動在後臺搞定一切,呼叫方法,編譯器會將OC程式碼轉換成執行時程式碼,在執行時確定資料結構和函式。
        2.通過Foundation框架的NSObject類定義的方法
        Cocoa程式中絕大部分類都是NSObject類的子類,所以都繼承了NSObject的行為。(NSProxy類是個例外,它是個抽象超類)
        一些情況下,NSObject類僅僅定義了完成某件事情的模板,並沒有提供所需要的程式碼。例如-description方法,該方法返回類內容的字串表示,該方法主要用來除錯程式。NSObject類並不知道子類的內容,所以它只是返回類的名字和物件的地址,NSObject的子類可以重新實現。
         還有一些NSObject的方法可以從Runtime系統中獲取資訊,允許物件進行自我檢查。例如:
         -class方法返回物件的類;
         -isKindOfClass: 和 -isMemberOfClass: 方法檢查物件是否存在於指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變數);
         -respondsToSelector: 檢查物件能否響應指定的訊息;
         -conformsToProtocol:檢查物件是否實現了指定協議類的方法;
         -methodForSelector: 返回指定方法實現的地址。
        3.通過對Runtime庫函式的直接呼叫
        Runtime系統是具有公共介面的動態共享庫。標頭檔案存放於/usr/include/objc目錄下,這意味著開發者使用時只需要引入objc/Runtime.h標頭檔案即可。
        許多函式可以讓你使用C語言來實現Objc中同樣的功能。除非是寫一些OC與其他語言的橋接或是底層的debug工作,開發者在寫OC程式碼時一般不會用到這些C語言函式。

        二、Runtime的相關術語

       1.SEL
       它是selector在OC中的表示(Swift中是Selector類)。selector是方法選擇器,其實作用就和名字一樣,日常生活中,我們通過人名辨別誰是誰,注意OC在相同的類中不會有命名相同的兩個方法。selector 對方法名進行包裝,以便找到對應的方法實現。它的資料結構是:typedef struct objc_selector *SEL;
       我們可以看出它是個對映到方法的C字串,你可以通過OC編譯器器命令@selector() 或者Runtime系統的sel_registerName函式來獲取一個SEL型別的方法選擇器。
       注意:不同類中相同名字的方法所對應的selector是相同的,由於變數的型別不同,所以不會導致它們呼叫方法實現混亂。


       2.id
       id是一個引數型別,它是指向某個類的例項的指標。定義如下:
       typedef struct objc_object *id;
       struct objc_object { Class isa; };
       由上定義,看到objc_object結構體包含一個isa指標,根據isa指標就可以找到物件所屬的類。
       注意:isa指標在程式碼執行時並不總指向例項物件所屬的型別,所以不能依靠它來確定型別,要想確定型別還是需要用物件的 -class 方法。KVO的實現機理就是將被觀察物件的isa指標指向一箇中間類而不是真實型別。


       3.Class
       typedef struct objc_class *Class;
       Class 其實是指向 objc_class 結構體的指標。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;

        從 objc_class 可以看到,一個執行時類中關聯了它的父類指標、類名、成員變數、方法、快取以及附屬的協議。


       其中objc_ivar_list 和objc_method_list分別是成員變數列表和方法列表:

// 成員變數列表

struct objc_ivar_list {

    int ivar_count                                           OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space                                                OBJC2_UNAVAILABLE;

#endif

    /* variable length structure */

    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;

}                                                            OBJC2_UNAVAILABLE;

// 方法列表

struct objc_method_list {

    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space                                                OBJC2_UNAVAILABLE;

#endif

    /* variable length structure */

    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;

}

        由此可見,我們可以動態修改 *methodList的值來新增成員方法,這也是Category實現的原理,同樣解釋了Category不能新增屬性的原因。
        objc_ivar_list結構體用來儲存成員變數的列表,而objc_ivar則是儲存了單個成員變數的資訊;同理,objc_method_list結構體儲存著方法陣列的列表,而單個方法的資訊則由objc_method結構體儲存。
       值得注意的時,objc_class中也有一個isa指標,這說明OC類本身也是一個物件。為了處理類和物件的關係,Runtime 庫建立了一種叫做 Meta Class(元類) 的東西,類物件所屬的類就叫做元類。Meta Class表述了類物件本身所具備的元資料。
       我們所熟悉的類方法,就源自於Meta Class。我們可以理解為類方法就是類物件的例項方法。每個類僅有一個類物件,而每個類物件僅有一個與之相關的元類。
       當你發出一個類似 [NSObject alloc](類方法) 的訊息時,實際上這個訊息被髮送給了一個類物件(Class Object),這個類物件必須是一個元類的例項,而這個元類同時也是一個根元類(Root Meta Class)的例項。所有元類的 isa指標最終都指向根元類。
        所以當 [NSObject alloc] 這條訊息傳送給類物件的時候,執行時程式碼 objc_msgSend() 會去它元類中查詢能夠響應訊息的方法實現,如果找到了,就會對這個類物件執行方法呼叫。
       最後 objc_class中還有一個objc_cache ,快取。

        4.Method
       Method 代表類中某個方法的型別

typedef struct objc_method *Method;

struct objc_method {

    SEL method_name                                          OBJC2_UNAVAILABLE;

    char *method_types                                       OBJC2_UNAVAILABLE;

    IMP method_imp                                           OBJC2_UNAVAILABLE;

}

objc_method 儲存了方法名,方法型別和方法實現:

方法名型別為 SEL

方法型別 method_types 是個 char 指標,儲存方法的引數型別和返回值型別

method_imp 指向了方法的實現,本質是一個函式指標

Ivar

Ivar 是表示成員變數的型別。

typedef struct objc_ivar *Ivar;

struct objc_ivar {

    char *ivar_name                                          OBJC2_UNAVAILABLE;

    char *ivar_type                                          OBJC2_UNAVAILABLE;

    int ivar_offset                                          OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space                                                OBJC2_UNAVAILABLE;

#endif

}

其中 ivar_offset 是基地址偏移位元組


        5.IMP
        IMP在objc.h中的定義是:
        typedef id (*IMP)(id, SEL, ...);
       它就是一個函式指標,這是由編譯器生成的。當你發起一個OC訊息之後,最終它會執行的那段程式碼,就是由這個函式指標指定的。而 IMP這個函式指標就指向了這個方法的實現。
        如果得到了執行某個例項某個方法的入口,我們就可以繞開訊息傳遞階段,直接執行方法。
        你會發現IMP指向的方法與objc_msgSend函式型別相同,引數都包含id和SEL型別。每個方法名都對應一個SEL型別的方法選擇器,而每個例項物件中的SEL對應的方法實現肯定是唯一的,通過一組id和SEL引數就能確定唯一的方法實現地址。而一個確定的方法也只有唯一的一組id和SEL引數。


         6.Cache

Cache 定義如下:

typedef struct objc_cache *Cache

struct objc_cache {

    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;

    unsigned int occupied                                    OBJC2_UNAVAILABLE;

    Method buckets[1]                                        OBJC2_UNAVAILABLE;

};

        Cache為方法呼叫的效能進行優化,每當例項物件接收到一個訊息時,它不會直接在 isa 指標指向的類的方法列表中遍歷查詢能夠響應的方法,因為每次都要查詢效率太低了,而是優先在 Cache中查詢。
       Runtime 系統會把被呼叫的方法存到 Cache中,如果一個方法被呼叫,那麼它有可能今後還會被呼叫,下次查詢的時候就會效率更高。就像計算機組成原理中 CPU繞過主存先訪問 Cache 一樣。


         7.Property

typedef struct objc_property *Property;

typedef struct objc_property *objc_property_t;//這個更常用

可以通過class_copyPropertyList 和 protocol_copyPropertyList 方法獲取類和協議中的屬性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

注意:

返回的是屬性列表,列表中每個元素都是一個 objc_property_t 指標

#import <Foundation/Foundation.h>

@interface Person : NSObject

/** 姓名 */

@property (strong, nonatomic) NSString *name;

/** age */

@property (assign, nonatomic) int age;

/** weight */

@property (assign, nonatomic) double weight;

@end

        以上是一個 Person 類,有3個屬性。讓我們用上述方法獲取類的執行時屬性。


    unsigned int outCount = 0;

    objc_property_t *properties = class_copyPropertyList([Person class], &outCount);

    NSLog(@"%d", outCount);

    for (NSInteger i = 0; i < outCount; i++) {

        NSString *name = @(property_getName(properties[i]));

        NSString *attributes = @(property_getAttributes(properties[i]));

        NSLog(@"%@--------%@", name, attributes);

    }

列印結果如下:

test[1525] 3

test[1525] [email protected]"NSString",&,N,V_name

test[1525] age--------Ti,N,V_age

test[1525] weight--------Td,N,V_weight

property_getName 用來查詢屬性的名稱,返回 c 字串。property_getAttributes 函式挖掘屬性的真實名稱和 @encode 型別,返回C字串。

objc_property_t class_getProperty(Class cls, const char *name)

objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

class_getProperty 和 protocol_getProperty 通過給出屬性名在類和協議中獲得屬性的引用。

           以上就是這篇Runtime博文的全部內容,歡迎關注三掌櫃微信公眾號,更多精彩等您來,歡迎關注!